更新补号机制

This commit is contained in:
扫地僧 2026-06-02 23:44:44 +08:00
parent e6b84aad80
commit 269fbd08ff
21 changed files with 2130 additions and 2112 deletions

View File

@ -91,7 +91,7 @@ func (c *ApiGetCardController) GetCard() {
} }
func (c *ApiGetCardController) extractCursor(platform, dataType string, now time.Time) { func (c *ApiGetCardController) extractCursor(platform, dataType string, now time.Time) {
for { c.extractWithProbe("cursor", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
var row models.PlatformAccountPoolCursor var row models.PlatformAccountPoolCursor
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)). qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
Filter("is_extracted", 0). Filter("is_extracted", 0).
@ -100,6 +100,55 @@ func (c *ApiGetCardController) extractCursor(platform, dataType string, now time
qs = qs.Filter("data_type", dataType) qs = qs.Filter("data_type", dataType)
} }
if err := qs.OrderBy("id").One(&row); err != nil { if err := qs.OrderBy("id").One(&row); err != nil {
return 0, nil, nil, "", "", nil, err
}
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, row.IsUsed, nil
})
}
func (c *ApiGetCardController) extractWindsurf(platform, dataType string, now time.Time) {
c.extractWithProbe("windsurf", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
var row models.PlatformAccountPoolWindsurf
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
Filter("is_extracted", 0).
Filter("delete_time__isnull", true)
if dataType != "" {
qs = qs.Filter("data_type", dataType)
}
if err := qs.OrderBy("id").One(&row); err != nil {
return 0, nil, nil, "", "", nil, err
}
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil
})
}
func (c *ApiGetCardController) extractKrio(platform, dataType string, now time.Time) {
c.extractWithProbe("krio", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
var row models.PlatformAccountPoolKiro
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
Filter("is_extracted", 0).
Filter("delete_time__isnull", true)
if dataType != "" {
qs = qs.Filter("data_type", dataType)
}
if err := qs.OrderBy("id").One(&row); err != nil {
return 0, nil, nil, "", "", nil, err
}
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil
})
}
type poolRowFetcher func() (id uint64, account, password *string, token, rowDataType string, isUsed *int8, err error)
// extractWithProbe 按 id 顺序提取并探测 Token 可用性;不可用则标记已提取并继续下一条。
func (c *ApiGetCardController) extractWithProbe(
module, platform, dataType string,
now time.Time,
fetch poolRowFetcher,
) {
for {
id, account, password, token, rowDataType, isUsed, err := fetch()
if err != nil {
if err == orm.ErrNoRows { if err == orm.ErrNoRows {
c.cardErr(404, 404, "暂无可用卡密") c.cardErr(404, 404, "暂无可用卡密")
} else { } else {
@ -108,85 +157,40 @@ func (c *ApiGetCardController) extractCursor(platform, dataType string, now time
return return
} }
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)). tableName := poolTableName(module)
Filter("id", row.ID). if tableName == "" {
c.cardErr(500, 500, "无效模块")
return
}
_, err = models.Orm.QueryTable(tableName).
Filter("id", id).
Update(map[string]interface{}{ Update(map[string]interface{}{
"is_extracted": 1, "is_extracted": 1,
"extracted_time": now, "extracted_time": now,
"extracted_platform": platform, "extracted_platform": platform,
"update_time": now,
}) })
if err != nil { if err != nil {
c.cardErr(500, 500, "提取失败") c.cardErr(500, 500, "提取失败")
return return
} }
// Cursor 号池需要先判断可用状态is_used=1 才发送给前端; // 已有探测结论:可用则直接返回,不可用则继续下一条。
// is_used=0已用完/不可用)或 NULL未探测则继续提取下一条。 if known, available := poolIsUsedAvailable(isUsed); known {
if row.IsUsed != nil && *row.IsUsed == 1 { if available {
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType)) c.cardOK(buildCardResult(account, password, token, rowDataType))
return return
}
continue
} }
}
}
func (c *ApiGetCardController) extractWindsurf(platform, dataType string, now time.Time) { if !poolProbeToken(module, rowDataType, token, id) {
var row models.PlatformAccountPoolWindsurf continue
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
Filter("is_extracted", 0).
Filter("delete_time__isnull", true)
if dataType != "" {
qs = qs.Filter("data_type", dataType)
}
if err := qs.OrderBy("id").One(&row); err != nil {
if err == orm.ErrNoRows {
c.cardErr(404, 404, "暂无可用卡密")
} else {
c.cardErr(500, 500, "查询失败")
} }
return
}
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
Filter("id", row.ID).
Update(map[string]interface{}{
"is_extracted": 1,
"extracted_time": now,
"extracted_platform": platform,
})
if err != nil {
c.cardErr(500, 500, "提取失败")
return
}
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType))
}
func (c *ApiGetCardController) extractKrio(platform, dataType string, now time.Time) { c.cardOK(buildCardResult(account, password, token, rowDataType))
var row models.PlatformAccountPoolKiro
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
Filter("is_extracted", 0).
Filter("delete_time__isnull", true)
if dataType != "" {
qs = qs.Filter("data_type", dataType)
}
if err := qs.OrderBy("id").One(&row); err != nil {
if err == orm.ErrNoRows {
c.cardErr(404, 404, "暂无可用卡密")
} else {
c.cardErr(500, 500, "查询失败")
}
return return
} }
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
Filter("id", row.ID).
Update(map[string]interface{}{
"is_extracted": 1,
"extracted_time": now,
"extracted_platform": platform,
})
if err != nil {
c.cardErr(500, 500, "提取失败")
return
}
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType))
} }
// buildCardResult 根据账号类型返回格式化字符串 // buildCardResult 根据账号类型返回格式化字符串

View File

@ -0,0 +1,954 @@
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"
)
// BackendArticleController CMS 文章管理
type BackendArticleController struct {
beego.Controller
}
// BackendArticleCategoryController CMS 文章分类管理
type BackendArticleCategoryController struct {
beego.Controller
}
func (c *BackendArticleController) cmsClaims() (*jwtutil.Claims, error) {
return cmsBackendClaims(&c.Controller)
}
func (c *BackendArticleCategoryController) cmsClaims() (*jwtutil.Claims, error) {
return cmsBackendClaims(&c.Controller)
}
func cmsBackendClaims(c *beego.Controller) (*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 != "backend" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
func cmsEffectiveTid(c *beego.Controller, claims *jwtutil.Claims) uint64 {
_ = c.ParseForm(1 << 20)
if tid, err := c.GetUint64("tid"); err == nil && tid > 0 {
return tid
}
if h := strings.TrimSpace(c.Ctx.Request.Header.Get("X-Tenant-Id")); h != "" {
if v, e := strconv.ParseUint(h, 10, 64); e == nil {
return v
}
}
if claims != nil && claims.TenantId > 0 {
return uint64(claims.TenantId)
}
return 0
}
func (c *BackendArticleController) 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 *BackendArticleCategoryController) cmsJSONErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func cmsEnsureTables(c *beego.Controller) bool {
if err := models.EnsureCmsArticleTables(); err != nil {
c.Ctx.Output.SetStatus(500)
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "初始化文章表失败: " + err.Error()}
_ = c.ServeJSON()
return false
}
return true
}
func cmsParseUintArg(v interface{}) uint64 {
switch x := v.(type) {
case float64:
if x > 0 {
return uint64(x)
}
case string:
if n, err := strconv.ParseUint(strings.TrimSpace(x), 10, 64); err == nil {
return n
}
}
return 0
}
func cmsArticleToListItem(row models.CmsArticle, cateName string) map[string]interface{} {
return map[string]interface{}{
"id": row.ID,
"title": row.Title,
"author": row.Author,
"cate": cateName,
"cate_id": row.CateID,
"status": row.Status,
"views": row.Views,
"likes": row.Likes,
"top": row.Top,
"recommend": row.Recommend,
"publish_date": models.CmsFormatTime(row.PublishTime),
"update_time": models.CmsFormatTime(row.UpdateTime),
}
}
func cmsArticleToDetail(row models.CmsArticle, cateName string) map[string]interface{} {
pub := models.CmsFormatTime(row.PublishTime)
return map[string]interface{}{
"id": row.ID,
"title": row.Title,
"author": row.Author,
"cate": cateName,
"cate_id": row.CateID,
"content": row.Content,
"desc": row.Desc,
"image": row.Image,
"is_trans": row.IsTrans,
"transurl": row.TransURL,
"status": row.Status,
"views": row.Views,
"view_count": row.Views,
"likes": row.Likes,
"top": row.Top,
"recommend": row.Recommend,
"publish_time": pub,
"publish_date": pub,
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
"update_time": models.CmsFormatTime(row.UpdateTime),
}
}
func cmsCategoryToMap(row models.CmsArticleCategory) map[string]interface{} {
return map[string]interface{}{
"id": row.ID,
"name": row.Name,
"label": row.Name,
"cid": row.Cid,
"parentId": row.Cid,
"image": row.Image,
"desc": row.Desc,
"remark": row.Desc,
"sort": row.Sort,
"status": row.Status,
}
}
// List GET /backend/articlesList
func (c *BackendArticleController) List() {
if !cmsEnsureTables(&c.Controller) {
return
}
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)
pageSize, _ := c.GetInt("pageSize", 10)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 200 {
pageSize = 200
}
keyword := strings.TrimSpace(c.GetString("keyword"))
cateFilter := strings.TrimSpace(c.GetString("cate"))
qs := models.Orm.QueryTable(new(models.CmsArticle)).
Filter("tid", tid).
Filter("delete_time__isnull", true)
if keyword != "" {
qs = qs.Filter("title__icontains", keyword)
}
if cateFilter != "" {
if cid, err := strconv.ParseUint(cateFilter, 10, 64); err == nil && cid > 0 {
qs = qs.Filter("cate_id", cid)
}
}
total, _ := qs.Count()
var rows []models.CmsArticle
offset := (page - 1) * pageSize
_, err = qs.OrderBy("-top", "-id").Limit(pageSize, offset).All(&rows)
if err != nil && err != orm.ErrNoRows {
c.cmsJSONErr(500, 500, "获取文章列表失败")
return
}
cateIDs := make([]uint64, 0, len(rows))
for _, r := range rows {
if r.CateID > 0 {
cateIDs = append(cateIDs, r.CateID)
}
}
cateNames := models.CmsCategoryNameMap(tid, cateIDs)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, cmsArticleToListItem(r, cateNames[r.CateID]))
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"list": list, "total": total},
}
_ = c.ServeJSON()
}
// ListAll GET /backend/allarticles
func (c *BackendArticleController) ListAll() {
c.List()
}
// Detail GET /backend/articles/:id
func (c *BackendArticleController) Detail() {
if !cmsEnsureTables(&c.Controller) {
return
}
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
}
var row models.CmsArticle
err = models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
One(&row)
if err == orm.ErrNoRows {
c.cmsJSONErr(404, 404, "文章不存在")
return
}
if err != nil {
c.cmsJSONErr(500, 500, "查询失败")
return
}
cateName := ""
if row.CateID > 0 {
names := models.CmsCategoryNameMap(tid, []uint64{row.CateID})
cateName = names[row.CateID]
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": cmsArticleToDetail(row, cateName),
}
_ = c.ServeJSON()
}
type cmsArticlePayload struct {
Title string `json:"title"`
Author string `json:"author"`
Cate interface{} `json:"cate"`
Content string `json:"content"`
Desc string `json:"desc"`
Image string `json:"image"`
IsTrans int8 `json:"is_trans"`
TransURL *string `json:"transurl"`
Status int8 `json:"status"`
IgnoreSimilarity int `json:"ignore_similarity"`
}
// Create POST /backend/createarticle
func (c *BackendArticleController) Create() {
if !cmsEnsureTables(&c.Controller) {
return
}
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 cmsArticlePayload
if err := json.Unmarshal(raw, &p); err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
title := strings.TrimSpace(p.Title)
if title == "" {
c.cmsJSONErr(400, 400, "标题不能为空")
return
}
if p.IgnoreSimilarity != 1 {
similar, serr := models.CmsSimilarArticles(tid, title, 5)
if serr == nil && len(similar) > 0 {
c.Ctx.Output.SetStatus(409)
c.Data["json"] = map[string]interface{}{
"code": 409,
"msg": "检测到相似标题",
"data": map[string]interface{}{"similar_articles": similar},
}
_ = c.ServeJSON()
return
}
}
now := time.Now()
cateID := cmsParseUintArg(p.Cate)
row := models.CmsArticle{
Tid: tid,
Title: title,
Author: strings.TrimSpace(p.Author),
CateID: cateID,
Content: p.Content,
Desc: strings.TrimSpace(p.Desc),
Image: strings.TrimSpace(p.Image),
IsTrans: p.IsTrans,
TransURL: p.TransURL,
Status: p.Status,
CreateTime: now,
UpdateTime: &now,
}
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/editarticle/:id
func (c *BackendArticleController) Update() {
if !cmsEnsureTables(&c.Controller) {
return
}
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 cmsArticlePayload
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),
"author": strings.TrimSpace(p.Author),
"cate_id": cmsParseUintArg(p.Cate),
"content": p.Content,
"desc": strings.TrimSpace(p.Desc),
"image": strings.TrimSpace(p.Image),
"is_trans": p.IsTrans,
"transurl": p.TransURL,
"status": p.Status,
"update_time": now,
}
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
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/deletearticle/:id
func (c *BackendArticleController) Delete() {
if !cmsEnsureTables(&c.Controller) {
return
}
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.CmsArticle)).
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()
}
func (c *BackendArticleController) setArticleFlag(field string, value int8) {
if !cmsEnsureTables(&c.Controller) {
return
}
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()
fields := map[string]interface{}{field: value, "update_time": now}
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
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": "success"}
_ = c.ServeJSON()
}
func (c *BackendArticleController) Publish() {
if !cmsEnsureTables(&c.Controller) {
return
}
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
}
var uid uint64
raw, _ := io.ReadAll(c.Ctx.Request.Body)
if len(raw) > 0 {
var body struct {
UID uint64 `json:"uid"`
}
_ = json.Unmarshal(raw, &body)
uid = body.UID
}
if uid == 0 && claims != nil {
uid = uint64(claims.UserID)
}
now := time.Now()
fields := map[string]interface{}{
"status": int8(2),
"publish_time": now,
"publisher_id": uid,
"update_time": now,
}
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
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()
}
func (c *BackendArticleController) Unpublish() {
if !cmsEnsureTables(&c.Controller) {
return
}
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.CmsArticle)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"status": int8(3), "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()
}
func (c *BackendArticleController) Recommend() { c.setArticleFlag("recommend", 1) }
func (c *BackendArticleController) Unrecommend() { c.setArticleFlag("recommend", 0) }
func (c *BackendArticleController) Top() { c.setArticleFlag("top", 1) }
func (c *BackendArticleController) Untop() { c.setArticleFlag("top", 0) }
// List GET /backend/categories
func (c *BackendArticleCategoryController) List() {
if !cmsEnsureTables(&c.Controller) {
return
}
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)
pageSize, _ := c.GetInt("pageSize", 0)
if pageSize == 0 {
pageSize, _ = c.GetInt("limit", 1000)
}
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 1000
}
keyword := strings.TrimSpace(c.GetString("keyword"))
qs := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("tid", tid).
Filter("delete_time__isnull", true)
if keyword != "" {
qs = qs.Filter("name__icontains", keyword)
}
total, _ := qs.Count()
var rows []models.CmsArticleCategory
offset := (page - 1) * pageSize
_, err = qs.OrderBy("sort", "id").Limit(pageSize, 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, cmsCategoryToMap(r))
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"list": list, "total": total, "records": list},
}
_ = c.ServeJSON()
}
// ListAll GET /backend/allcategories
func (c *BackendArticleCategoryController) ListAll() {
if !cmsEnsureTables(&c.Controller) {
return
}
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
}
keyword := strings.TrimSpace(c.GetString("keyword"))
qs := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("tid", tid).
Filter("delete_time__isnull", true)
if keyword != "" {
qs = qs.Filter("name__icontains", keyword)
}
var rows []models.CmsArticleCategory
_, err = qs.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, cmsCategoryToMap(r))
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": list}
_ = c.ServeJSON()
}
// Detail GET /backend/categories/:id
func (c *BackendArticleCategoryController) Detail() {
if !cmsEnsureTables(&c.Controller) {
return
}
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
}
var row models.CmsArticleCategory
err = models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
One(&row)
if err == orm.ErrNoRows {
c.cmsJSONErr(404, 404, "分类不存在")
return
}
if err != nil {
c.cmsJSONErr(500, 500, "查询失败")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": cmsCategoryToMap(row)}
_ = c.ServeJSON()
}
type cmsCategoryPayload struct {
Name string `json:"name"`
Image string `json:"image"`
Desc string `json:"desc"`
Sort int `json:"sort"`
Status int8 `json:"status"`
Cid uint64 `json:"cid"`
}
// Create POST /backend/createCategory
func (c *BackendArticleCategoryController) Create() {
if !cmsEnsureTables(&c.Controller) {
return
}
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 cmsCategoryPayload
if err := json.Unmarshal(raw, &p); err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
name := strings.TrimSpace(p.Name)
if name == "" {
c.cmsJSONErr(400, 400, "分类名称不能为空")
return
}
now := time.Now()
row := models.CmsArticleCategory{
Tid: tid,
Cid: p.Cid,
Name: name,
Image: strings.TrimSpace(p.Image),
Desc: strings.TrimSpace(p.Desc),
Sort: p.Sort,
Status: p.Status,
CreateTime: now,
UpdateTime: &now,
}
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/editCategory/:id
func (c *BackendArticleCategoryController) Update() {
if !cmsEnsureTables(&c.Controller) {
return
}
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 cmsCategoryPayload
if err := json.Unmarshal(raw, &p); err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{
"name": strings.TrimSpace(p.Name),
"image": strings.TrimSpace(p.Image),
"desc": strings.TrimSpace(p.Desc),
"sort": p.Sort,
"status": p.Status,
"cid": p.Cid,
"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()
}
// Delete DELETE /backend/categories/:id
func (c *BackendArticleCategoryController) Delete() {
if !cmsEnsureTables(&c.Controller) {
return
}
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
}
childCnt, _ := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("tid", tid).
Filter("cid", id).
Filter("delete_time__isnull", true).
Count()
if childCnt > 0 {
c.cmsJSONErr(400, 400, "请先删除子分类")
return
}
articleCnt, _ := models.Orm.QueryTable(new(models.CmsArticle)).
Filter("tid", tid).
Filter("cate_id", id).
Filter("delete_time__isnull", true).
Count()
if articleCnt > 0 {
c.cmsJSONErr(400, 400, "该分类下还有文章,无法删除")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
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()
}
// UpdateStatus PATCH /backend/categories/:id/status
func (c *BackendArticleCategoryController) UpdateStatus() {
if !cmsEnsureTables(&c.Controller) {
return
}
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 struct {
Status int8 `json:"status"`
}
if err := json.Unmarshal(raw, &p); err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"status": p.Status, "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()
}

View File

@ -0,0 +1,600 @@
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"
)
// BackendDomainPoolController 主域名池管理
type BackendDomainPoolController struct {
beego.Controller
}
// BackendTenantDomainController 租户域名管理
type BackendTenantDomainController struct {
beego.Controller
}
func requireBackend(c *beego.Controller) (*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 != "backend" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
// ===== 主域名池 =====
// Index GET /backend/domain/pool/index?page=&pageSize=&main_domain=&status=
func (c *BackendDomainPoolController) Index() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 10)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 200 {
pageSize = 200
}
mainDomain := strings.TrimSpace(c.GetString("main_domain"))
statusStr := strings.TrimSpace(c.GetString("status"))
qs := models.Orm.QueryTable(new(models.SystemDomainPool)).Filter("delete_time__isnull", true)
if mainDomain != "" {
qs = qs.Filter("main_domain__icontains", mainDomain)
}
if statusStr != "" {
if st, err := strconv.Atoi(statusStr); err == nil {
qs = qs.Filter("status", st)
}
}
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取主域名池失败: "+err.Error())
return
}
var rows []models.SystemDomainPool
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取主域名池失败: "+err.Error())
return
}
list := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
item := map[string]interface{}{
"id": rows[i].ID,
"main_domain": rows[i].MainDomain,
"status": rows[i].Status,
"create_time": rows[i].CreateTime.Format("2006-01-02 15:04:05"),
"update_time": "",
}
if rows[i].UpdateTime != nil {
item["update_time"] = rows[i].UpdateTime.Format("2006-01-02 15:04:05")
}
list = append(list, item)
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"list": list,
"total": total,
},
}
_ = c.ServeJSON()
}
// GetEnabledDomains GET /backend/domain/pool/getEnabledDomains
func (c *BackendDomainPoolController) GetEnabledDomains() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
var rows []models.SystemDomainPool
_, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("status", 1).
Filter("delete_time__isnull", true).
OrderBy("-id").
All(&rows)
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取主域名失败: "+err.Error())
return
}
out := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
out = append(out, map[string]interface{}{
"id": rows[i].ID,
"main_domain": rows[i].MainDomain,
"status": rows[i].Status,
})
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
_ = c.ServeJSON()
}
// Create POST /backend/domain/pool/create
func (c *BackendDomainPoolController) Create() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p domainPoolPayload
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
md := strings.TrimSpace(p.MainDomain)
if md == "" {
jsonErr(&c.Controller, 400, 400, "主域名不能为空")
return
}
if p.Status != 0 && p.Status != 1 {
p.Status = 1
}
// 简单去重
cnt, _ := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("main_domain", md).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
jsonErr(&c.Controller, 400, 400, "主域名已存在")
return
}
row := &models.SystemDomainPool{MainDomain: md, Status: p.Status}
if _, err := models.Orm.Insert(row); err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功"}
_ = c.ServeJSON()
}
// Update POST /backend/domain/pool/update
func (c *BackendDomainPoolController) Update() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p domainPoolPayload
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
if p.ID == 0 {
jsonErr(&c.Controller, 400, 400, "id 不能为空")
return
}
md := strings.TrimSpace(p.MainDomain)
if md == "" {
jsonErr(&c.Controller, 400, 400, "主域名不能为空")
return
}
if p.Status != 0 && p.Status != 1 {
p.Status = 1
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("id", p.ID).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"main_domain": md, "status": p.Status, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "记录不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
_ = c.ServeJSON()
}
// Delete DELETE /backend/domain/pool/delete/:id
func (c *BackendDomainPoolController) Delete() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
jsonErr(&c.Controller, 400, 400, "无效ID")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "记录不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
_ = c.ServeJSON()
}
// ToggleStatus POST /backend/domain/pool/toggleStatus body:{id}
func (c *BackendDomainPoolController) ToggleStatus() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p struct {
ID uint64 `json:"id"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var row models.SystemDomainPool
if err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("id", p.ID).
Filter("delete_time__isnull", true).
One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "记录不存在")
return
}
newStatus := int8(1)
if row.Status == 1 {
newStatus = 0
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("id", p.ID).
Update(map[string]interface{}{"status": newStatus, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "切换失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// ===== 租户域名 =====
// Index GET /backend/domain/tenant/index?page=&pageSize=&tid=&status=&sub_domain=
func (c *BackendTenantDomainController) Index() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 10)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 200 {
pageSize = 200
}
tid, _ := c.GetUint64("tid")
statusStr := strings.TrimSpace(c.GetString("status"))
subDomain := strings.TrimSpace(c.GetString("sub_domain"))
qs := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("delete_time__isnull", true)
if tid > 0 {
qs = qs.Filter("tid", tid)
}
if statusStr != "" {
if st, err := strconv.Atoi(statusStr); err == nil {
qs = qs.Filter("status", st)
}
}
if subDomain != "" {
qs = qs.Filter("sub_domain__icontains", subDomain)
}
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取租户域名失败: "+err.Error())
return
}
var rows []models.SystemTenantDomain
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取租户域名失败: "+err.Error())
return
}
list := make([]models.SystemTenantDomain, 0, len(rows))
list = append(list, rows...)
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"list": list, "total": total},
}
_ = c.ServeJSON()
}
// MyDomains GET /backend/domain/tenant/myDomains?tid=1
func (c *BackendTenantDomainController) MyDomains() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
tid, _ := c.GetUint64("tid")
if tid == 0 {
jsonErr(&c.Controller, 400, 400, "租户ID不能为空")
return
}
var rows []models.SystemTenantDomain
_, err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("tid", tid).
Filter("delete_time__isnull", true).
OrderBy("-id").
All(&rows)
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": rows}
_ = c.ServeJSON()
}
// Apply POST /backend/domain/tenant/apply body:{tid,sub_domain,main_domain}
func (c *BackendTenantDomainController) Apply() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p struct {
Tid uint64 `json:"tid"`
SubDomain string `json:"sub_domain"`
MainDomain string `json:"main_domain"`
}
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
if p.Tid == 0 {
jsonErr(&c.Controller, 400, 400, "租户ID不能为空")
return
}
sub := strings.TrimSpace(p.SubDomain)
main := strings.TrimSpace(p.MainDomain)
if sub == "" {
jsonErr(&c.Controller, 400, 400, "二级域名前缀不能为空")
return
}
if main == "" {
jsonErr(&c.Controller, 400, 400, "请选择主域名")
return
}
if !subDomainRe.MatchString(sub) {
jsonErr(&c.Controller, 400, 400, "二级域名前缀格式不正确")
return
}
// 该租户是否已有域名
cnt, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("tid", p.Tid).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
jsonErr(&c.Controller, 400, 400, "该租户已有域名,请删除后再次申请")
return
}
// 主域名存在且启用
var pool models.SystemDomainPool
if err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("main_domain", main).
Filter("status", 1).
Filter("delete_time__isnull", true).
One(&pool); err != nil {
jsonErr(&c.Controller, 400, 400, "主域名不存在或已禁用")
return
}
// 二级域名是否已被使用(同主域名下)
used, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("sub_domain", sub).
Filter("main_domain", main).
Filter("delete_time__isnull", true).
Count()
if used > 0 {
jsonErr(&c.Controller, 400, 400, "该二级域名已被使用")
return
}
full := sub + "." + main
now := time.Now()
tid := p.Tid
row := &models.SystemTenantDomain{
Tid: &tid,
SubDomain: &sub,
MainDomain: &main,
FullDomain: &full,
Status: 0,
CreateTime: now,
UpdateTime: &now,
}
id, err := models.Orm.Insert(row)
if err != nil {
jsonErr(&c.Controller, 500, 500, "申请失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "申请提交成功,等待审核", "data": map[string]interface{}{"id": uint64(id)}}
_ = c.ServeJSON()
}
// Audit POST /backend/domain/tenant/audit body:{id,action} action=approve/reject
func (c *BackendTenantDomainController) Audit() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p struct {
ID uint64 `json:"id"`
Action string `json:"action"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var row models.SystemTenantDomain
if err := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "域名不存在")
return
}
if row.Status != 0 {
jsonErr(&c.Controller, 400, 400, "该域名已审核过了")
return
}
newStatus := 2
msg := "已拒绝"
if strings.ToLower(strings.TrimSpace(p.Action)) == "approve" {
newStatus = 1
msg = "审核通过"
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).Update(map[string]interface{}{
"status": newStatus,
"update_time": now,
})
if err != nil {
jsonErr(&c.Controller, 500, 500, "审核失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": msg}
_ = c.ServeJSON()
}
// ToggleStatus POST /backend/domain/tenant/toggleStatus body:{id}
func (c *BackendTenantDomainController) ToggleStatus() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p struct {
ID uint64 `json:"id"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var row models.SystemTenantDomain
if err := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "域名不存在")
return
}
if row.Status == 0 {
jsonErr(&c.Controller, 400, 400, "审核中不可操作")
return
}
newStatus := 2
if row.Status == 2 {
newStatus = 1
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).Update(map[string]interface{}{
"status": newStatus,
"update_time": now,
})
if err != nil {
jsonErr(&c.Controller, 500, 500, "操作失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// Delete DELETE /backend/domain/tenant/delete/:id
func (c *BackendTenantDomainController) Delete() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "域名不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
_ = c.ServeJSON()
}
// 用于复杂筛选时可扩展:当前保留 orm.Condition import避免被 gofmt 删除
var _ = orm.NewCondition

View File

@ -0,0 +1,21 @@
package controllers
import (
"regexp"
beego "github.com/beego/beego/v2/server/web"
)
func jsonErr(c *beego.Controller, httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
type domainPoolPayload struct {
ID uint64 `json:"id"`
MainDomain string `json:"main_domain"`
Status int8 `json:"status"`
}
var subDomainRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`)

View File

@ -619,69 +619,150 @@ func replenishPoolRow(c *beego.Controller, module string) {
platform := payload.Platform platform := payload.Platform
remark := strings.TrimSpace(payload.Remark) remark := strings.TrimSpace(payload.Remark)
replenishWithProbe(c, module, payload.Type, platform, remark, now)
}
type poolReplenishCandidate struct {
id uint64
dataType string
token string
isUsed *int8
row interface{}
}
type poolReplenishFetcher func() (*poolReplenishCandidate, error)
// replenishWithProbe 按 id 顺序补号并探测;不可用则标记 is_extracted=2 后继续下一条。
func replenishWithProbe(c *beego.Controller, module, dataType, platform, remark string, now time.Time) {
var fetch poolReplenishFetcher
switch module { switch module {
case "cursor": case "cursor":
var row models.PlatformAccountPoolCursor fetch = func() (*poolReplenishCandidate, error) {
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)). var row models.PlatformAccountPoolCursor
Filter("is_extracted", 0).Filter("data_type", payload.Type). err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
OrderBy("id").One(&row); err != nil { Filter("is_extracted", 0).
poolJSONErr(c, 404, 404, "暂无可用账号") Filter("data_type", dataType).
return Filter("delete_time__isnull", true).
OrderBy("id").
One(&row)
if err != nil {
return nil, err
}
return &poolReplenishCandidate{
id: row.ID, dataType: row.DataType, token: row.Token, isUsed: row.IsUsed, row: row,
}, nil
} }
if _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", row.ID).Update(map[string]interface{}{
"is_extracted": int8(2), "extracted_time": now, "extracted_platform": platform, "remark": remark,
}); err != nil {
poolJSONErr(c, 500, 500, "补号失败: "+err.Error())
return
}
row.IsExtracted = 2
row.ExtractedTime = &now
row.ExtractedPlatform = &platform
row.Remark = remark
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "补号成功", "data": row}
case "windsurf": case "windsurf":
var row models.PlatformAccountPoolWindsurf fetch = func() (*poolReplenishCandidate, error) {
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)). var row models.PlatformAccountPoolWindsurf
Filter("is_extracted", 0).Filter("data_type", payload.Type). err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
OrderBy("id").One(&row); err != nil { Filter("is_extracted", 0).
poolJSONErr(c, 404, 404, "暂无可用账号") Filter("data_type", dataType).
return Filter("delete_time__isnull", true).
OrderBy("id").
One(&row)
if err != nil {
return nil, err
}
return &poolReplenishCandidate{
id: row.ID, dataType: row.DataType, token: row.Token, row: row,
}, nil
} }
if _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).Filter("id", row.ID).Update(map[string]interface{}{
"is_extracted": int8(2), "extracted_time": now, "extracted_platform": platform, "remark": remark,
}); err != nil {
poolJSONErr(c, 500, 500, "补号失败: "+err.Error())
return
}
row.IsExtracted = 2
row.ExtractedTime = &now
row.ExtractedPlatform = &platform
row.Remark = remark
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "补号成功", "data": row}
case "krio": case "krio":
var row models.PlatformAccountPoolKiro fetch = func() (*poolReplenishCandidate, error) {
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)). var row models.PlatformAccountPoolKiro
Filter("is_extracted", 0).Filter("data_type", payload.Type). err := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
OrderBy("id").One(&row); err != nil { Filter("is_extracted", 0).
poolJSONErr(c, 404, 404, "暂无可用账号") Filter("data_type", dataType).
return Filter("delete_time__isnull", true).
OrderBy("id").
One(&row)
if err != nil {
return nil, err
}
return &poolReplenishCandidate{
id: row.ID, dataType: row.DataType, token: row.Token, row: row,
}, nil
} }
if _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).Filter("id", row.ID).Update(map[string]interface{}{
"is_extracted": int8(2), "extracted_time": now, "extracted_platform": platform, "remark": remark,
}); err != nil {
poolJSONErr(c, 500, 500, "补号失败: "+err.Error())
return
}
row.IsExtracted = 2
row.ExtractedTime = &now
row.ExtractedPlatform = &platform
row.Remark = remark
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "补号成功", "data": row}
default: default:
poolJSONErr(c, 400, 400, "无效模块") poolJSONErr(c, 400, 400, "无效模块")
return return
} }
_ = c.ServeJSON()
tableName := poolTableName(module)
if tableName == "" {
poolJSONErr(c, 400, 400, "无效模块")
return
}
for {
candidate, err := fetch()
if err != nil {
if err == orm.ErrNoRows {
poolJSONErr(c, 404, 404, "暂无可用账号")
} else {
poolJSONErr(c, 500, 500, "查询失败")
}
return
}
updateFields := map[string]interface{}{
"is_extracted": int8(2),
"extracted_time": now,
"extracted_platform": platform,
"remark": remark,
"update_time": now,
}
if _, err = models.Orm.QueryTable(tableName).
Filter("id", candidate.id).
Update(updateFields); err != nil {
poolJSONErr(c, 500, 500, "补号失败: "+err.Error())
return
}
if known, available := poolIsUsedAvailable(candidate.isUsed); known {
if !available {
continue
}
} else if !poolProbeToken(module, candidate.dataType, candidate.token, candidate.id) {
continue
}
data := replenishApplyResponse(candidate.row, platform, remark, now)
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "补号成功", "data": data}
_ = c.ServeJSON()
return
}
}
func replenishApplyResponse(row interface{}, platform, remark string, now time.Time) interface{} {
pf := platform
switch r := row.(type) {
case models.PlatformAccountPoolCursor:
r.IsExtracted = 2
r.ExtractedTime = &now
r.ExtractedPlatform = &pf
r.Remark = remark
if r.IsUsed == nil || *r.IsUsed != 1 {
used := int8(1)
r.IsUsed = &used
}
return r
case models.PlatformAccountPoolWindsurf:
r.IsExtracted = 2
r.ExtractedTime = &now
r.ExtractedPlatform = &pf
r.Remark = remark
return r
case models.PlatformAccountPoolKiro:
r.IsExtracted = 2
r.ExtractedTime = &now
r.ExtractedPlatform = &pf
r.Remark = remark
return r
default:
return row
}
} }
func updatePoolRemark(c *beego.Controller, module string) { func updatePoolRemark(c *beego.Controller, module string) {

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -45,12 +44,6 @@ func requirePlatform(c *beego.Controller) (*jwtutil.Claims, error) {
return claims, nil return claims, nil
} }
func jsonErr(c *beego.Controller, httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
// ===== 主域名池 ===== // ===== 主域名池 =====
// Index GET /platform/domain/pool/index?page=&pageSize=&main_domain=&status= // Index GET /platform/domain/pool/index?page=&pageSize=&main_domain=&status=
@ -150,12 +143,6 @@ func (c *PlatformDomainPoolController) GetEnabledDomains() {
_ = c.ServeJSON() _ = c.ServeJSON()
} }
type domainPoolPayload struct {
ID uint64 `json:"id"`
MainDomain string `json:"main_domain"`
Status int8 `json:"status"`
}
// Create POST /platform/domain/pool/create // Create POST /platform/domain/pool/create
func (c *PlatformDomainPoolController) Create() { func (c *PlatformDomainPoolController) Create() {
if _, err := requirePlatform(&c.Controller); err != nil { if _, err := requirePlatform(&c.Controller); err != nil {
@ -397,8 +384,6 @@ func (c *PlatformTenantDomainController) MyDomains() {
_ = c.ServeJSON() _ = c.ServeJSON()
} }
var subDomainRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`)
// Apply POST /platform/domain/tenant/apply body:{tid,sub_domain,main_domain} // Apply POST /platform/domain/tenant/apply body:{tid,sub_domain,main_domain}
func (c *PlatformTenantDomainController) Apply() { func (c *PlatformTenantDomainController) Apply() {
if _, err := requirePlatform(&c.Controller); err != nil { if _, err := requirePlatform(&c.Controller); err != nil {

63
controllers/pool_probe.go Normal file
View File

@ -0,0 +1,63 @@
package controllers
import (
"strings"
"time"
"server/models"
"server/pkg/tokenprobe"
)
func poolTableName(module string) string {
switch module {
case "cursor":
return new(models.PlatformAccountPoolCursor).TableName()
case "windsurf":
return new(models.PlatformAccountPoolWindsurf).TableName()
case "krio":
return new(models.PlatformAccountPoolKiro).TableName()
default:
return ""
}
}
// poolNeedsTokenProbe account 类型无 Token无需探测tk / account_tk 需探测。
func poolNeedsTokenProbe(dataType, token string) bool {
if strings.TrimSpace(token) == "" {
return false
}
return dataType != "account"
}
func poolSaveCursorIsUsed(id uint64, isUsed int8) {
_, _ = models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
Filter("id", id).
Update(map[string]interface{}{
"is_used": isUsed,
"update_time": time.Now(),
})
}
// poolProbeToken 探测 Tokencursor 模块会回写 is_used。
func poolProbeToken(module, dataType, token string, rowID uint64) bool {
if !poolNeedsTokenProbe(dataType, token) {
return true
}
r := tokenprobe.ProbeOfficial(module, token)
if module == "cursor" && rowID > 0 {
var isUsed int8
if r.OK {
isUsed = 1
}
poolSaveCursorIsUsed(rowID, isUsed)
}
return r.OK
}
// poolIsUsedAvailable 已有探测结论时1=可用0=不可用nil=未探测。
func poolIsUsedAvailable(isUsed *int8) (known bool, available bool) {
if isUsed == nil {
return false, false
}
return true, *isUsed == 1
}

View File

@ -1,392 +0,0 @@
# 存储功能部署检查清单
## 部署前准备
### 1. 环境检查
- [ ] Go 1.17+ 已安装
- [ ] MySQL 5.7+ 已安装并运行
- [ ] Node.js 14+ 已安装(前端)
- [ ] 网络连接正常
### 2. 依赖安装
```bash
# 后端依赖
cd go
go mod download
go mod tidy
# 前端依赖(如需要)
cd platform
npm install
```
### 3. 数据库迁移
```bash
# 备份数据库
mysqldump -u root -p your_database > backup_$(date +%Y%m%d).sql
# 执行迁移
mysql -u root -p your_database < go/migrations/add_storage_config_table.sql
# 验证表创建
mysql -u root -p your_database -e "SHOW TABLES LIKE 'yz_system_storage_config';"
mysql -u root -p your_database -e "DESC yz_system_storage_config;"
```
## 部署步骤
### 1. 后端部署
```bash
cd go
# 编译
go build -o server main.go
# 或使用bee工具
bee run
```
### 2. 前端部署
```bash
cd platform
# 开发环境
npm run dev
# 生产环境
npm run build
```
### 3. 配置验证
访问http://localhost:8080/platform/storageConfig
预期响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"storage_type": "local",
...
}
}
```
## 功能测试
### 1. 存储配置测试
#### 测试本地存储
1. 登录平台管理后台
2. 进入:系统设置 → 平台设置 → 存储配置
3. 选择"本地存储"
4. 点击"保存设置"
5. 验证保存成功
#### 测试七牛云存储
1. 准备七牛云账号和配置信息
2. 选择"七牛云存储"
3. 填写配置:
- AccessKey: `your_access_key`
- SecretKey: `your_secret_key`
- Bucket: `your_bucket`
- CDN域名: `https://cdn.example.com`
- 存储区域: `z0`
4. 点击"保存设置"
5. 验证保存成功
### 2. 文件上传测试
#### 本地存储上传
1. 配置为本地存储
2. 上传测试文件
3. 检查文件是否保存到 `uploads/` 目录
4. 验证文件URL格式`/uploads/2024/01/01/xxx.jpg`
5. 访问文件URL确认可以访问
#### 七牛云上传
1. 配置为七牛云存储
2. 上传测试文件
3. 检查数据库记录
4. 验证文件URL格式`https://cdn.example.com/2024/01/01/xxx.jpg`
5. 访问文件URL确认可以访问
### 3. 文件迁移测试
1. 准备一些本地存储的文件
2. 配置七牛云存储
3. 调用迁移API
```bash
curl -X POST http://localhost:8080/platform/storage/migrateToQiniu
```
4. 检查迁移进度和结果
5. 验证文件URL已更新
6. 访问新URL确认文件可访问
## 性能测试
### 1. 上传性能
```bash
# 测试单文件上传
time curl -F "file=@test.jpg" http://localhost:8080/platform/uploadfile
# 测试批量上传
for i in {1..10}; do
curl -F "file=@test$i.jpg" http://localhost:8080/platform/uploadfile &
done
wait
```
### 2. 迁移性能
- 准备100个测试文件
- 执行迁移
- 记录总耗时
- 计算平均速度
## 监控检查
### 1. 日志检查
```bash
# 查看服务日志
tail -f logs/server.log
# 查看错误日志
grep ERROR logs/server.log
# 查看上传日志
grep "文件上传" logs/server.log
```
### 2. 数据库检查
```sql
-- 检查存储配置
SELECT * FROM yz_system_storage_config;
-- 检查文件记录
SELECT COUNT(*) FROM yz_system_files;
-- 检查最近上传的文件
SELECT * FROM yz_system_files ORDER BY create_time DESC LIMIT 10;
```
### 3. 存储空间检查
```bash
# 本地存储空间
du -sh uploads/
# 七牛云存储空间(在七牛云控制台查看)
```
## 安全检查
### 1. 配置安全
- [ ] SecretKey 不在日志中输出
- [ ] 配置文件权限正确600
- [ ] 数据库连接使用强密码
- [ ] API接口有认证保护
### 2. 文件安全
- [ ] 文件大小限制生效200MB
- [ ] 文件类型验证正常
- [ ] 恶意文件上传被拦截
- [ ] 文件访问权限正确
### 3. 网络安全
- [ ] HTTPS配置正确
- [ ] CDN域名已备案
- [ ] 防火墙规则正确
- [ ] 跨域配置正确
## 回滚计划
### 如果部署失败
1. 停止服务
```bash
pkill -f server
```
2. 恢复数据库
```bash
mysql -u root -p your_database < backup_YYYYMMDD.sql
```
3. 恢复代码
```bash
git checkout previous_version
```
4. 重启服务
```bash
cd go
bee run
```
## 常见问题
### 问题1: 依赖安装失败
**解决方法:**
```bash
# 清理缓存
go clean -modcache
# 使用代理
export GOPROXY=https://goproxy.cn,direct
# 重新安装
go mod download
```
### 问题2: 数据库迁移失败
**解决方法:**
```bash
# 检查表是否已存在
mysql -u root -p your_database -e "SHOW TABLES LIKE 'yz_system_storage_config';"
# 如果存在,先删除
mysql -u root -p your_database -e "DROP TABLE IF EXISTS yz_system_storage_config;"
# 重新执行迁移
mysql -u root -p your_database < go/migrations/add_storage_config_table.sql
```
### 问题3: 七牛云上传失败
**检查项:**
- AccessKey 和 SecretKey 是否正确
- Bucket 是否存在
- 存储区域是否匹配
- 网络连接是否正常
**测试连接:**
```bash
curl -I https://your-cdn-domain.com
```
### 问题4: 文件访问404
**本地存储:**
```bash
# 检查文件是否存在
ls -la uploads/2024/01/01/
# 检查Nginx配置
nginx -t
# 检查文件权限
chmod 644 uploads/2024/01/01/*
```
**七牛云:**
- 检查CDN域名是否正确
- 检查文件是否上传成功
- 检查空间访问权限
## 部署完成确认
### 功能确认
- [ ] 存储配置页面正常显示
- [ ] 本地存储配置保存成功
- [ ] 七牛云配置保存成功
- [ ] 本地存储上传正常
- [ ] 七牛云上传正常
- [ ] 文件访问正常
- [ ] 文件迁移功能正常
- [ ] 错误处理正常
- [ ] 日志记录正常
### 性能确认
- [ ] 上传速度正常(< 5秒/10MB
- [ ] 访问速度正常(< 1秒
- [ ] 迁移速度正常(> 10文件/秒)
- [ ] 内存使用正常(< 500MB
- [ ] CPU使用正常< 50%
### 安全确认
- [ ] 认证保护生效
- [ ] 文件大小限制生效
- [ ] 文件类型验证生效
- [ ] 敏感信息不泄露
- [ ] 日志不包含密钥
## 上线通知
### 通知内容
```
【系统升级通知】
尊敬的用户:
系统已完成存储功能升级,新增以下功能:
1. 支持七牛云存储
2. 支持存储配置管理
3. 支持文件迁移功能
升级后的优势:
- 更快的访问速度CDN加速
- 更高的可靠性(云端备份)
- 更低的成本(按需付费)
如有问题,请联系技术支持。
感谢您的支持!
```
## 后续优化
### 短期优化1周内
- [ ] 添加上传进度显示
- [ ] 添加批量上传功能
- [ ] 优化错误提示
- [ ] 添加使用统计
### 中期优化1个月内
- [ ] 添加图片压缩
- [ ] 添加缩略图生成
- [ ] 添加水印功能
- [ ] 添加访问统计
### 长期优化3个月内
- [ ] 支持更多存储服务
- [ ] 添加文件管理界面
- [ ] 添加自动备份
- [ ] 添加CDN配置
---
**部署完成后,请在此签名确认:**
- 部署人员__________
- 部署时间__________
- 测试人员__________
- 测试时间__________
- 审核人员__________
- 审核时间__________

View File

@ -1,404 +0,0 @@
# 🎉 存储配置功能 - 完整实现报告
## 项目概述
本项目已完整实现文件存储的配置、上传和迁移功能,支持本地存储和七牛云存储的无缝切换。
## ✅ 完成的工作清单
### 1. 数据库层 (100%)
- ✅ 创建 `yz_system_storage_config`
- ✅ 编写数据库迁移SQL
- ✅ 添加默认配置数据
**文件:**
- `go/migrations/add_storage_config_table.sql`
### 2. 后端核心服务 (100%)
#### 存储服务抽象层
- ✅ 定义 `StorageService` 接口
- ✅ 实现 `LocalStorage` 本地存储
- ✅ 实现 `QiniuStorage` 七牛云存储
- ✅ 实现 `GetStorageService()` 自动选择
**文件:**
- `go/services/storage_service.go` (新增, 300+ 行)
**功能:**
- 统一的上传接口
- 自动MD5计算
- 支持所有七牛云区域
- 完整的错误处理
#### 文件迁移服务
- ✅ 实现并发迁移逻辑
- ✅ 实现进度跟踪
- ✅ 实现错误收集
- ✅ 实现数据库更新
**文件:**
- `go/services/storage_migration.go` (新增, 200+ 行)
**功能:**
- 5个并发迁移
- 实时进度显示
- 错误详细记录
- 自动回滚机制
### 3. 后端控制器 (100%)
#### 存储配置控制器
- ✅ 获取存储配置 API
- ✅ 保存存储配置 API
- ✅ 参数验证
- ✅ 错误处理
**文件:**
- `go/controllers/storage_config.go` (新增, 150+ 行)
#### 迁移控制器
- ✅ 迁移到七牛云 API
- ✅ 查询迁移进度 API
**文件:**
- `go/controllers/storage_migration.go` (新增, 60+ 行)
#### 文件上传控制器改造
- ✅ 集成存储服务
- ✅ 自动选择存储方式
- ✅ MD5去重检查
- ✅ 失败自动回滚
**文件:**
- `go/controllers/platform_file.go` (修改, 重构上传逻辑)
### 4. 后端模型和路由 (100%)
- ✅ 创建 `StorageConfig` 模型
- ✅ 注册模型到ORM
- ✅ 添加存储配置路由
- ✅ 添加迁移路由
**文件:**
- `go/models/storage_config.go` (新增)
- `go/models/init.go` (修改)
- `go/routers/platform/platform.go` (修改)
### 5. 依赖管理 (100%)
- ✅ 添加七牛云SDK依赖
- ✅ 更新 go.mod
- ✅ 创建依赖安装脚本
**文件:**
- `go/go.mod` (修改)
- `go/scripts/install_dependencies.sh` (新增)
- `go/scripts/install_dependencies.bat` (新增)
### 6. 前端实现 (100%)
#### API接口
- ✅ 获取存储配置接口
- ✅ 保存存储配置接口
**文件:**
- `platform/src/api/sitesettings.js` (修改)
#### 配置组件
- ✅ 存储类型切换
- ✅ 七牛云配置表单
- ✅ 表单验证
- ✅ 本地草稿保存
- ✅ 友好的提示信息
**文件:**
- `platform/src/views/system/platformsettings/components/storageSettings.vue` (新增, 250+ 行)
#### 主页面
- ✅ 添加存储配置标签页
- ✅ 集成配置组件
**文件:**
- `platform/src/views/system/platformsettings/index.vue` (修改)
### 7. 文档和脚本 (100%)
- ✅ 详细使用指南
- ✅ 实现总结文档
- ✅ 部署检查清单
- ✅ 测试脚本
- ✅ README文档
**文件:**
- `docs/storage-config-guide.md` (新增)
- `README_STORAGE.md` (新增)
- `DEPLOYMENT_CHECKLIST.md` (新增)
- `go/scripts/test_storage.sh` (新增)
- `IMPLEMENTATION_COMPLETE.md` (本文件)
## 📊 代码统计
### 新增文件
| 类型 | 文件数 | 代码行数 |
|------|--------|---------|
| Go后端 | 4 | ~800行 |
| Vue前端 | 1 | ~250行 |
| SQL | 1 | ~20行 |
| 脚本 | 3 | ~150行 |
| 文档 | 5 | ~2000行 |
| **总计** | **14** | **~3220行** |
### 修改文件
| 文件 | 修改内容 |
|------|---------|
| `go/controllers/platform_file.go` | 重构上传逻辑 |
| `go/models/init.go` | 注册新模型 |
| `go/routers/platform/platform.go` | 添加路由 |
| `go/go.mod` | 添加依赖 |
| `platform/src/api/sitesettings.js` | 添加API |
| `platform/src/views/system/platformsettings/index.vue` | 添加标签页 |
## 🎯 核心功能
### 1. 存储服务抽象
```go
type StorageService interface {
Upload(file, header) (*UploadResult, error)
GetPublicURL(key string) string
Delete(key string) error
}
```
### 2. 自动选择存储
```go
storageService, _ := services.GetStorageService()
// 根据配置自动返回 LocalStorage 或 QiniuStorage
```
### 3. 统一上传接口
```go
result, err := storageService.Upload(file, header)
// 返回统一的 UploadResult包含URL、Key、Size、MD5
```
### 4. 文件迁移
```go
progress, err := services.MigrateLocalToQiniu(tenantID)
// 并发迁移,实时进度,错误收集
```
## 🔧 技术栈
### 后端
- Go 1.17+
- Beego v2.1.0
- 七牛云SDK v7.18.2
- MySQL 5.7+
### 前端
- Vue 3
- Element Plus
- Axios
## 📦 部署步骤
### 1. 安装依赖
```bash
cd go
go mod download
go mod tidy
```
### 2. 数据库迁移
```bash
mysql -u root -p your_database < go/migrations/add_storage_config_table.sql
```
### 3. 启动服务
```bash
cd go
bee run
```
### 4. 配置存储
访问:平台管理后台 → 系统设置 → 平台设置 → 存储配置
## 🧪 测试覆盖
### 单元测试
- [ ] 存储服务接口测试
- [ ] 本地存储上传测试
- [ ] 七牛云上传测试
- [ ] 迁移服务测试
### 集成测试
- [x] API接口测试
- [x] 文件上传测试
- [x] 配置保存测试
- [x] 前端界面测试
### 性能测试
- [ ] 上传性能测试
- [ ] 并发上传测试
- [ ] 迁移性能测试
## 📈 性能指标
### 上传性能
- 本地存储:~50MB/s
- 七牛云:~10MB/s受网络影响
### 迁移性能
- 并发数5
- 速度:~10文件/秒
- 内存占用:< 100MB
## 🔒 安全特性
- ✅ 参数验证
- ✅ 文件大小限制200MB
- ✅ 文件类型验证
- ✅ MD5去重
- ✅ 错误处理
- ✅ 失败回滚
- ⚠️ 密钥加密(待实现)
## 🚀 扩展性
### 支持的存储类型
- ✅ 本地存储
- ✅ 七牛云存储
- ⏳ 阿里云OSS待实现
- ⏳ 腾讯云COS待实现
- ⏳ AWS S3待实现
### 可扩展功能
- ⏳ 图片压缩
- ⏳ 缩略图生成
- ⏳ 水印添加
- ⏳ 视频转码
- ⏳ 断点续传
- ⏳ 分片上传
## 📝 使用示例
### 配置本地存储
```javascript
{
storage_type: "local"
}
```
### 配置七牛云
```javascript
{
storage_type: "qiniu",
qiniu_access_key: "your_key",
qiniu_secret_key: "your_secret",
qiniu_bucket: "your_bucket",
qiniu_domain: "https://cdn.example.com",
qiniu_region: "z0"
}
```
### 上传文件
```go
// 自动选择存储
storageService, _ := services.GetStorageService()
result, _ := storageService.Upload(file, header)
fmt.Println(result.URL) // 完整访问URL
```
### 迁移文件
```go
progress, _ := services.MigrateLocalToQiniu(tenantID)
fmt.Printf("成功: %d, 失败: %d\n", progress.Success, progress.Failed)
```
## 🐛 已知问题
1. ⚠️ 密钥明文存储(建议加密)
2. ⚠️ 迁移进度查询未实现需要Redis或全局变量
3. ⚠️ 从七牛云迁移到本地未实现
## 📅 后续计划
### 短期1周
- [ ] 添加密钥加密
- [ ] 实现迁移进度查询
- [ ] 添加单元测试
### 中期1个月
- [ ] 支持阿里云OSS
- [ ] 支持腾讯云COS
- [ ] 添加图片处理功能
### 长期3个月
- [ ] 支持AWS S3
- [ ] 添加文件管理界面
- [ ] 添加访问统计
- [ ] 添加自动备份
## 🎓 学习资源
- 七牛云文档https://developer.qiniu.com/
- Go SDK文档https://github.com/qiniu/go-sdk
- Beego文档https://beego.vip/
- Vue3文档https://vuejs.org/
## 👥 贡献者
- 开发AI Assistant
- 测试:待定
- 文档AI Assistant
## 📄 许可证
本项目遵循项目原有许可证。
---
## ✨ 总结
本次实现完成了:
1. ✅ **完整的存储服务抽象层**,支持多种存储方式
2. ✅ **自动化的文件上传**,根据配置自动选择存储
3. ✅ **强大的文件迁移功能**,支持并发迁移和进度跟踪
4. ✅ **友好的配置界面**,简单易用的前端配置
5. ✅ **完善的文档**,包括使用指南、部署清单、测试脚本
**代码质量:**
- 清晰的架构设计
- 完整的错误处理
- 详细的代码注释
- 统一的代码风格
**可维护性:**
- 模块化设计
- 接口抽象
- 易于扩展
- 文档完善
**生产就绪:**
- 完整的功能实现
- 详细的部署文档
- 测试脚本
- 故障排查指南
---
**🎉 项目已完成,可以投入生产使用!**
如有问题,请参考:
- 使用指南:`docs/storage-config-guide.md`
- 部署清单:`DEPLOYMENT_CHECKLIST.md`
- 快速开始:`README_STORAGE.md`

View File

@ -1,122 +0,0 @@
# 🚀 存储配置功能 - 5分钟快速开始
## 第一步安装依赖1分钟
```bash
cd go
go mod download
go mod tidy
```
## 第二步数据库迁移1分钟
```bash
mysql -u root -p your_database < go/migrations/add_storage_config_table.sql
```
验证:
```bash
mysql -u root -p your_database -e "DESC yz_system_storage_config;"
```
## 第三步启动服务1分钟
```bash
cd go
bee run
# 或
go run main.go
```
## 第四步配置存储2分钟
### 方式1使用本地存储无需配置
1. 访问http://localhost:8080/#/system/platformsettings
2. 点击"存储配置"标签
3. 选择"本地存储"
4. 点击"保存设置"
✅ 完成!文件将保存到 `uploads/` 目录
### 方式2使用七牛云存储
1. 访问http://localhost:8080/#/system/platformsettings
2. 点击"存储配置"标签
3. 选择"七牛云存储"
4. 填写配置:
```
AccessKey: 你的AccessKey
SecretKey: 你的SecretKey
Bucket: 你的Bucket名称
CDN域名: https://你的CDN域名
存储区域: z0华东
```
5. 点击"保存设置"
✅ 完成!文件将上传到七牛云
## 测试上传
### 使用Postman测试
```
POST http://localhost:8080/platform/uploadfile
Headers:
Authorization: Bearer your_token
Body:
form-data
file: 选择文件
```
### 使用curl测试
```bash
curl -X POST \
-H "Authorization: Bearer your_token" \
-F "file=@test.jpg" \
http://localhost:8080/platform/uploadfile
```
## 常见问题
### Q1: 依赖安装失败?
```bash
export GOPROXY=https://goproxy.cn,direct
go mod download
```
### Q2: 数据库连接失败?
检查 `go/conf/app.conf` 中的数据库配置:
```ini
mysqluser = root
mysqlpass = your_password
mysqlurls = 127.0.0.1:3306
mysqldb = your_database
```
### Q3: 七牛云上传失败?
1. 检查密钥是否正确
2. 检查Bucket是否存在
3. 检查存储区域是否匹配
4. 测试网络连接:`curl -I https://你的CDN域名`
## 下一步
- 📖 阅读完整文档:`README_STORAGE.md`
- 🔧 查看部署清单:`DEPLOYMENT_CHECKLIST.md`
- 📚 查看使用指南:`docs/storage-config-guide.md`
- ✅ 查看实现报告:`IMPLEMENTATION_COMPLETE.md`
## 获取帮助
- 查看日志:`tail -f logs/server.log`
- 查看错误:`grep ERROR logs/server.log`
- 七牛云文档https://developer.qiniu.com/
---
**🎉 恭喜!你已经完成了存储配置功能的快速开始!**

View File

@ -1,210 +0,0 @@
# 存储配置功能 - 完整实现
## ✅ 已完成的所有工作
本项目已完整实现文件存储的配置、上传和迁移功能,支持本地存储和七牛云存储。
## 快速开始
### 1. 安装依赖
```bash
cd go
go mod download
go mod tidy
```
或使用脚本:
- Linux/Mac: `./scripts/install_dependencies.sh`
- Windows: `scripts\install_dependencies.bat`
### 2. 执行数据库迁移
```bash
mysql -u root -p your_database < migrations/add_storage_config_table.sql
```
### 3. 重启服务
```bash
bee run
```
### 4. 配置存储
访问:平台管理后台 → 系统设置 → 平台设置 → 存储配置
## 核心功能
### ✅ 1. 存储服务抽象层
**文件**: `services/storage_service.go`
- 统一的存储接口 `StorageService`
- 本地存储实现 `LocalStorage`
- 七牛云存储实现 `QiniuStorage`
- 自动选择存储服务 `GetStorageService()`
- 支持所有七牛云存储区域
### ✅ 2. 文件上传改造
**文件**: `controllers/platform_file.go`
- 自动根据配置选择存储方式
- MD5去重检查
- 失败自动回滚
- 完整的错误处理
### ✅ 3. 文件迁移功能
**文件**: `services/storage_migration.go`
- 从本地迁移到七牛云
- 并发迁移5个并发
- 实时进度跟踪
- 错误收集和报告
### ✅ 4. 存储配置管理
**后端**:
- `models/storage_config.go` - 数据模型
- `controllers/storage_config.go` - API控制器
**前端**:
- `platform/src/views/system/platformsettings/components/storageSettings.vue` - 配置界面
### ✅ 5. API接口
**存储配置**:
- `GET /platform/storageConfig` - 获取配置
- `POST /platform/saveStorageConfig` - 保存配置
**文件上传**:
- `POST /platform/uploadfile` - 上传文件(自动选择存储)
**文件迁移**:
- `POST /platform/storage/migrateToQiniu` - 迁移到七牛云
- `GET /platform/storage/migrationProgress` - 查询进度
## 技术实现
### 存储服务架构
```
┌─────────────────────────────────────┐
│ File Upload Controller │
│ (platform_file.go) │
└──────────────┬──────────────────────┘
┌─────────────────────────────────────┐
│ Storage Service Interface │
│ (storage_service.go) │
└──────────┬──────────────────────────┘
┌──────┴──────┐
│ │
▼ ▼
┌─────────┐ ┌──────────┐
│ Local │ │ Qiniu │
│ Storage │ │ Storage │
└─────────┘ └──────────┘
```
## 七牛云配置
### 存储区域
| 区域名称 | 代码 |
|---------|------|
| 华东-浙江 | z0 |
| 华北-河北 | z1 |
| 华南-广东 | z2 |
| 北美-洛杉矶 | na0 |
| 亚太-新加坡 | as0 |
| 华东-浙江2 | cn-east-2 |
### 配置步骤
1. 注册七牛云账号
2. 创建存储空间Bucket
3. 获取 AccessKey 和 SecretKey
4. 配置 CDN 域名
5. 在系统中填写配置
## 文件清单
### 后端核心文件
```
go/
├── models/
│ ├── storage_config.go # 存储配置模型
│ └── init.go # 模型注册(已修改)
├── controllers/
│ ├── storage_config.go # 存储配置控制器
│ ├── storage_migration.go # 迁移控制器
│ └── platform_file.go # 文件上传(已改造)
├── services/
│ ├── storage_service.go # 存储服务(核心)
│ └── storage_migration.go # 迁移服务
├── routers/
│ └── platform/platform.go # 路由注册(已修改)
├── migrations/
│ └── add_storage_config_table.sql # 数据库迁移
├── scripts/
│ ├── install_dependencies.sh # 依赖安装Linux/Mac
│ └── install_dependencies.bat # 依赖安装Windows
└── go.mod # 依赖管理已添加七牛云SDK
```
## 使用示例
### 配置本地存储
```javascript
{
storage_type: "local"
}
```
### 配置七牛云存储
```javascript
{
storage_type: "qiniu",
qiniu_access_key: "your_access_key",
qiniu_secret_key: "your_secret_key",
qiniu_bucket: "your_bucket",
qiniu_domain: "https://cdn.example.com",
qiniu_region: "z0"
}
```
### 上传文件
```go
// 后端自动选择存储
storageService, _ := services.GetStorageService()
result, _ := storageService.Upload(file, header)
// result.URL 是完整的访问URL
```
### 迁移文件
```go
// 迁移到七牛云
progress, err := services.MigrateLocalToQiniu(tenantID)
fmt.Printf("成功: %d, 失败: %d\n", progress.Success, progress.Failed)
```
## 更多文档
- 详细使用指南:`docs/storage-config-guide.md`
- 部署检查清单:`docs/DEPLOYMENT_CHECKLIST.md`
- 快速开始:`docs/QUICK_START.md`
- 实现报告:`docs/IMPLEMENTATION_COMPLETE.md`
---
**所有功能已完整实现并测试通过!** 🎉

View File

@ -1,253 +0,0 @@
# 存储配置功能说明
## 功能概述
系统支持两种文件存储方式:
1. **本地存储**:文件存储在服务器本地磁盘
2. **七牛云存储**:文件存储在七牛云对象存储服务
## 数据库变更
### 新增表
**表名**: `yz_system_storage_config`
**字段说明**:
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | bigint(20) | 主键ID |
| storage_type | varchar(20) | 存储类型: local/qiniu |
| qiniu_access_key | varchar(255) | 七牛云AccessKey |
| qiniu_secret_key | varchar(255) | 七牛云SecretKey |
| qiniu_bucket | varchar(128) | 七牛云Bucket名称 |
| qiniu_domain | varchar(255) | 七牛云CDN域名 |
| qiniu_region | varchar(50) | 七牛云存储区域 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
### 执行迁移
```bash
# 在MySQL中执行迁移脚本
mysql -u your_user -p your_database < go/migrations/add_storage_config_table.sql
```
## 后端实现
### 新增文件
1. **模型文件**: `go/models/storage_config.go`
- 定义 `StorageConfig` 模型
- 提供 `GetStorageConfig()` 方法获取配置
2. **控制器文件**: `go/controllers/storage_config.go`
- `GetStorageConfig`: 获取存储配置
- `SaveStorageConfig`: 保存存储配置
3. **路由注册**: `go/routers/platform/platform.go`
```go
beego.Router("/platform/storageConfig", &controllers.StorageConfigController{}, "get:GetStorageConfig")
beego.Router("/platform/saveStorageConfig", &controllers.StorageConfigController{}, "post:SaveStorageConfig")
```
### API接口
#### 获取存储配置
```
GET /platform/storageConfig
```
**响应示例**:
```json
{
"code": 200,
"msg": "success",
"data": {
"storage_type": "qiniu",
"qiniu_access_key": "your_access_key",
"qiniu_secret_key": "your_secret_key",
"qiniu_bucket": "your_bucket",
"qiniu_domain": "https://cdn.example.com",
"qiniu_region": "z0"
}
}
```
#### 保存存储配置
```
POST /platform/saveStorageConfig
```
**请求体**:
```json
{
"storage_type": "qiniu",
"qiniu_access_key": "your_access_key",
"qiniu_secret_key": "your_secret_key",
"qiniu_bucket": "your_bucket",
"qiniu_domain": "https://cdn.example.com",
"qiniu_region": "z0"
}
```
## 前端实现
### 新增文件
1. **API文件**: `platform/src/api/sitesettings.js`
- 新增 `getStorageConfig()` 方法
- 新增 `saveStorageConfig()` 方法
2. **组件文件**: `platform/src/views/system/platformsettings/components/storageSettings.vue`
- 存储配置表单组件
- 支持本地存储和七牛云存储切换
- 表单验证和数据持久化
3. **页面更新**: `platform/src/views/system/platformsettings/index.vue`
- 新增"存储配置"标签页
### 使用说明
1. 登录平台管理后台
2. 进入"系统设置" -> "平台设置"
3. 切换到"存储配置"标签页
4. 选择存储类型:
- **本地存储**:无需额外配置
- **七牛云存储**:需要填写以下信息
### 七牛云配置步骤
1. **注册七牛云账号**
- 访问 https://www.qiniu.com/
- 注册并完成实名认证
2. **创建存储空间**
- 登录七牛云控制台
- 进入"对象存储" -> "空间管理"
- 点击"新建空间"
- 填写空间名称Bucket
- 选择存储区域
- 设置访问控制(建议选择"公开"
3. **获取密钥**
- 进入"个人中心" -> "密钥管理"
- 查看或创建 AccessKey 和 SecretKey
4. **配置CDN域名**
- 在存储空间详情页,进入"域名管理"
- 添加自定义域名或使用测试域名
- 完成域名备案和CNAME解析
- 获取CDN加速域名
5. **填写配置信息**
- AccessKey: 从密钥管理获取
- SecretKey: 从密钥管理获取
- Bucket: 存储空间名称
- CDN域名: 完整的域名地址(如 https://cdn.example.com
- 存储区域: 选择创建空间时的区域
### 存储区域对照表
| 区域名称 | 区域代码 |
|---------|---------|
| 华东-浙江 | z0 |
| 华北-河北 | z1 |
| 华南-广东 | z2 |
| 北美-洛杉矶 | na0 |
| 亚太-新加坡 | as0 |
| 华东-浙江2 | cn-east-2 |
## 后续开发建议
### 文件上传服务改造
需要修改文件上传相关的代码,根据 `storage_type` 选择不同的存储方式:
```go
// 示例代码
func UploadFile(file *multipart.FileHeader) (string, error) {
cfg, _ := models.GetStorageConfig()
switch cfg.StorageType {
case "qiniu":
return uploadToQiniu(file, cfg)
case "local":
return uploadToLocal(file)
default:
return uploadToLocal(file)
}
}
```
### 七牛云SDK集成
需要安装七牛云Go SDK
```bash
go get github.com/qiniu/go-sdk/v7
```
示例上传代码:
```go
import (
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
)
func uploadToQiniu(file *multipart.FileHeader, cfg *models.StorageConfig) (string, error) {
mac := qbox.NewMac(cfg.QiniuAccessKey, cfg.QiniuSecretKey)
putPolicy := storage.PutPolicy{
Scope: cfg.QiniuBucket,
}
upToken := putPolicy.UploadToken(mac)
// 配置上传参数
cfg := storage.Config{
Zone: &storage.ZoneHuadong, // 根据 cfg.QiniuRegion 选择
UseHTTPS: true,
UseCdnDomains: false,
}
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
// 执行上传
err := formUploader.PutFile(context.Background(), &ret, upToken, key, localFile, nil)
if err != nil {
return "", err
}
// 返回完整URL
return cfg.QiniuDomain + "/" + ret.Key, nil
}
```
## 注意事项
1. **安全性**
- SecretKey 在数据库中明文存储,建议后续加密处理
- 生产环境建议使用环境变量或密钥管理服务
2. **成本**
- 七牛云存储和流量会产生费用
- 建议设置合理的存储策略和CDN缓存规则
3. **迁移**
- 切换存储方式时,已有文件不会自动迁移
- 需要手动迁移或保持双存储支持
4. **备份**
- 重要文件建议定期备份
- 七牛云支持跨区域备份功能
## 测试清单
- [ ] 数据库表创建成功
- [ ] 后端API接口正常
- [ ] 前端页面显示正常
- [ ] 本地存储配置保存成功
- [ ] 七牛云配置保存成功
- [ ] 表单验证正常工作
- [ ] 配置切换功能正常
- [ ] 数据持久化正常

View File

@ -1,200 +0,0 @@
# 修复请求体为空问题
## 问题描述
七牛云上传成功后,保存文件记录到数据库时失败:
```
POST https://api.yunzer.cn/platform/qiniu/save 400
{"code": 400, "msg": "参数解析失败: 请求体为空"}
```
## 问题原因
Beego 框架默认不会复制请求体到 `c.Ctx.Input.RequestBody`,需要显式启用 `CopyRequestBody` 配置。
## 解决方案
### 1. 修改 go/conf/app.conf
添加配置:
```properties
# 启用请求体复制(允许多次读取请求体)
copyrequestbody = true
```
### 2. 修改 go/main.go
在代码中显式启用:
```go
func main() {
// 初始化数据库
models.Init(version.Version)
// 启用请求体复制(允许多次读取请求体)
beego.BConfig.CopyRequestBody = true // ← 新增
// 设置最大请求体大小10MB足够登录请求使用
beego.BConfig.MaxMemory = 10 << 20 // 10MB
// 静态资源:映射 /uploads 到本地 uploads 目录,供前端访问上传文件
beego.SetStaticPath("/uploads", "uploads")
beego.Run()
}
```
### 3. 添加调试日志
`go/controllers/qiniu_upload.go``SaveFileRecord` 方法中添加日志:
```go
// 调试:打印请求体
body := c.Ctx.Input.RequestBody
fmt.Println("SaveFileRecord 请求体长度:", len(body))
fmt.Println("SaveFileRecord 请求体内容:", string(body))
```
## 重启服务
```bash
# 重启 Go 服务
systemctl restart go-api
# 查看服务状态
systemctl status go-api
# 查看日志
tail -f /www/wwwroot/api.yunzer.cn/go.log
```
## 测试步骤
1. 重启后端服务
2. 登录前端系统
3. 进入软件升级页面
4. 上传一个文件
5. 观察后端日志
### 预期日志输出
```
SaveFileRecord 请求体长度: 123
SaveFileRecord 请求体内容: {"key":"2026/04/09/xxx.exe","hash":"xxx","size":60742452,"name":"xxx.exe","mimeType":"application/x-msdownload","cate":0}
```
### 预期响应
```json
{
"code": 200,
"data": {
"url": "http://7colud.yunzer.cn/2026/04/09/xxx.exe",
"id": 123,
"name": "xxx.exe",
"key": "2026/04/09/xxx.exe"
}
}
```
## 相关配置说明
### CopyRequestBody 的作用
Beego 框架中,请求体默认只能读取一次。如果需要在多个地方读取请求体(例如中间件和控制器),需要启用 `CopyRequestBody`
启用后Beego 会在接收到请求时将请求体复制到 `c.Ctx.Input.RequestBody`,允许多次读取。
### 配置方式
有两种方式启用:
1. **配置文件方式** (`go/conf/app.conf`):
```properties
copyrequestbody = true
```
2. **代码方式** (`go/main.go`):
```go
beego.BConfig.CopyRequestBody = true
```
建议两种方式都配置,确保生效。
## 注意事项
### 1. 内存占用
启用 `CopyRequestBody` 会增加内存占用,因为每个请求的请求体都会被复制到内存中。
对于大文件上传,建议:
- 使用七牛云直传(不经过服务器)
- 只在需要的接口启用请求体复制
### 2. 与登录接口的兼容性
之前修复登录问题时,我们已经将登录接口改为使用 `c.Ctx.Input.RequestBody`,启用 `CopyRequestBody` 后,登录接口也能正常工作。
### 3. MaxMemory 配置
`MaxMemory` 配置控制请求体的最大大小:
```go
beego.BConfig.MaxMemory = 10 << 20 // 10MB
```
对于七牛云直传,文件不经过服务器,所以这个限制不影响大文件上传。
## 验证修复
### 1. 检查配置是否生效
重启服务后,查看日志中是否有请求体内容输出。
### 2. 测试上传功能
上传一个文件,检查:
- 七牛云上传是否成功
- 数据库记录是否保存成功
- 文件 URL 是否正确
### 3. 检查数据库
```sql
SELECT id, name, src, size, type, cate, md5, create_time
FROM system_file
ORDER BY id DESC
LIMIT 5;
```
应该能看到新上传的文件记录。
## 回滚方案
如果修复后出现其他问题,可以临时禁用:
```go
// go/main.go
beego.BConfig.CopyRequestBody = false
```
或在 `go/conf/app.conf` 中:
```properties
copyrequestbody = false
```
然后重启服务。
## 相关文件
- `go/main.go` - 主程序入口
- `go/conf/app.conf` - 配置文件
- `go/controllers/qiniu_upload.go` - 七牛云上传控制器
- `go/controllers/platform_auth.go` - 登录控制器(也使用 RequestBody
## 更新日期
2026-04-09

View File

@ -1,122 +0,0 @@
# 快速修复:请求体为空问题
## 问题
```
POST /platform/qiniu/save 400
{"code": 400, "msg": "参数解析失败: 请求体为空"}
```
## 快速修复步骤
### 1. 重启后端服务(已修改配置)
```bash
systemctl restart go-api
```
### 2. 查看服务状态
```bash
systemctl status go-api
```
预期输出:
```
● go-api.service - Go API Server
Loaded: loaded
Active: active (running)
```
### 3. 查看日志
```bash
tail -f /www/wwwroot/api.yunzer.cn/go.log
```
### 4. 测试上传
1. 登录系统
2. 进入软件升级页面
3. 上传一个文件
### 5. 观察日志输出
应该看到:
```
SaveFileRecord 请求体长度: xxx
SaveFileRecord 请求体内容: {"key":"...","hash":"...","size":...}
```
## 已修改的文件
`go/main.go` - 添加 `beego.BConfig.CopyRequestBody = true`
`go/conf/app.conf` - 添加 `copyrequestbody = true`
`go/controllers/qiniu_upload.go` - 添加调试日志
## 如果还是失败
### 检查 1: 服务是否重启成功
```bash
systemctl status go-api
```
如果失败,查看错误:
```bash
journalctl -u go-api -n 50
```
### 检查 2: 配置是否生效
查看日志中是否有请求体内容输出。如果没有,说明配置未生效。
### 检查 3: 前端请求是否正确
打开浏览器开发者工具,查看 Network 标签:
- 请求方法POST
- Content-Type: application/json
- 请求体:应该有 JSON 数据
## 完整上传流程
```
1. 前端上传文件到七牛云 ✓
2. 七牛云返回文件信息 ✓
{
"key": "2026/04/09/xxx.exe",
"hash": "xxx",
"size": 60742452
}
3. 前端调用 /platform/qiniu/save ← 这里失败了
POST /platform/qiniu/save
Body: {
"key": "...",
"hash": "...",
"size": ...,
"name": "...",
"mimeType": "...",
"cate": 0
}
4. 后端保存到数据库 ← 修复后应该成功
5. 返回文件 URL
```
## 修复原理
Beego 框架默认不复制请求体,需要启用 `CopyRequestBody`
```go
// 修复前
c.Ctx.Input.RequestBody // 空的
// 修复后(启用 CopyRequestBody
c.Ctx.Input.RequestBody // 包含请求体数据
```
## 更新时间
2026-04-09

View File

@ -1,26 +0,0 @@
## 接口文件
> 约定:每新增一个对外接口,都需要在本文件登记(端/方法/路径/描述/鉴权/入出参简述)。
### platform平台端
| 方法 | 路径 | 描述 |
|---|---|---|
| `POST` | `/platform/login` | 平台登录 |
| `POST` | `/platform/sendLoginCode` | 发送登录验证码 |
| `POST` | `/platform/loginBySms` | 手机号验证码登录 |
| `POST` | `/platform/logout` | 平台退出登录 |
| `GET` | `/platform/login/getGeetest3Infos` | 获取极验3.0配置 |
| `GET` | `/platform/login/getGeetest4Infos` | 获取极验4.0配置 |
| `GET` | `/platform/login/getOpenVerify` | 判断是否开启登录验证 |
| `POST` | `/platform/resetPassword` | 忘记密码重置 |
| `POST` | `/platform/sendResetCode` | 发送找回密码验证码 |
#### `/platform/login` 详情
- 入参JSON body`{ "username": string, "password": string }`
- 出参JSON`{ "success": boolean, "token": string }`
- 说明:当前使用占位登录逻辑,仅校验非空并返回平台用户 JWT后续接真实用户/租户表
> 其余 `/platform/*` 登录相关接口(发送验证码、极验、重置密码等)目前仅返回 `501 Not Implemented`,后续按实际需求逐步补全。

View File

@ -1,218 +0,0 @@
# 立即执行:重启服务
## 修复内容
✅ 已修复日志错误(`beego.Info` → `fmt.Println`
✅ 已启用 `CopyRequestBody` 配置
✅ 已添加调试输出
## 立即执行
### 1. 重启服务
```bash
systemctl restart go-api
```
### 2. 检查服务状态
```bash
systemctl status go-api
```
**预期输出**:
```
● go-api.service - Go API Server
Active: active (running)
```
如果显示 `failed`,查看错误:
```bash
journalctl -u go-api -n 50
```
### 3. 查看实时日志
```bash
tail -f /www/wwwroot/api.yunzer.cn/go.log
```
或者查看标准输出(调试日志会输出到这里):
```bash
journalctl -u go-api -f
```
### 4. 测试上传
1. 打开浏览器,登录系统
2. 进入:平台管理 → 软件升级
3. 点击"新增"或"编辑"
4. 上传一个文件(建议先用小文件测试)
### 5. 观察日志
在终端中应该看到:
```
SaveFileRecord 请求体长度: 150
SaveFileRecord 请求体内容: {"key":"2026/04/09/1775732976777726699.exe","hash":"loozoz7qv9flWXsS5UldWdPX9-T_","size":60742452,"name":"xxx.exe","mimeType":"application/x-msdownload","cate":0}
```
### 6. 验证结果
**前端应该显示**:
- 上传进度条
- 上传成功提示
- 文件 URL: `http://7colud.yunzer.cn/2026/04/09/xxxxx.exe`
**数据库验证**:
```bash
mysql -u go-platform -p -h 212.64.112.158 -P 3388 go-platform
```
```sql
SELECT id, name, src, size, type, create_time
FROM system_file
ORDER BY id DESC
LIMIT 5;
```
## 完整上传流程
```
用户选择文件
前端获取存储配置
storageType: 'qiniu'
前端获取上传凭证
token, region: 'z2'
前端直接上传到七牛云
POST https://upload-z2.qiniup.com
✓ 成功返回: {key, hash, size}
前端保存文件记录
POST /platform/qiniu/save
Body: {key, hash, size, name, mimeType, cate}
后端接收请求
✓ CopyRequestBody 已启用
✓ 请求体不为空
后端保存到数据库
INSERT INTO system_file
返回文件信息
{url, id, name, key}
前端显示上传成功
```
## 如果还是失败
### 问题 1: 服务启动失败
**检查**:
```bash
journalctl -u go-api -n 50
```
**常见原因**:
- 端口被占用
- 数据库连接失败
- 配置文件错误
### 问题 2: 请求体仍然为空
**检查**:
1. 确认服务已重启
2. 查看日志中是否有 "请求体长度: 0"
3. 检查前端请求的 Content-Type 是否为 `application/json`
**解决**:
```bash
# 确保配置生效
grep -i "copyrequestbody" /www/wwwroot/api.yunzer.cn/conf/app.conf
# 应该看到
copyrequestbody = true
```
### 问题 3: 七牛云上传失败
**检查**:
- 浏览器控制台是否有 CORS 错误
- 七牛云 bucket 是否存在
- 区域配置是否正确z2
**解决**:
参见 `platform/docs/七牛云上传测试步骤.md`
## 调试技巧
### 1. 查看完整请求
浏览器开发者工具 → Network 标签 → 找到 `/platform/qiniu/save` 请求:
- Headers: 查看 Content-Type
- Payload: 查看请求体内容
- Response: 查看响应内容
### 2. 查看后端日志
```bash
# 实时日志
tail -f /www/wwwroot/api.yunzer.cn/go.log
# 或者查看 systemd 日志(包含 fmt.Println 输出)
journalctl -u go-api -f
```
### 3. 测试 API
使用 curl 测试:
```bash
# 获取 token先登录
TOKEN="your_token_here"
# 测试保存接口
curl -X POST https://api.yunzer.cn/platform/qiniu/save \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"key": "test/test.txt",
"hash": "test123",
"size": 1024,
"name": "test.txt",
"mimeType": "text/plain",
"cate": 0
}'
```
## 成功标志
✓ 服务启动成功
✓ 日志中看到请求体内容
✓ 前端显示上传成功
✓ 数据库有新记录
✓ 文件 URL 可以访问
## 下一步
上传成功后,可以:
1. 移除调试日志(`fmt.Println`
2. 测试大文件上传
3. 测试批量上传
4. 验证文件去重功能
## 联系支持
如果问题仍然存在,请提供:
1. 服务状态输出
2. 完整的错误日志
3. 浏览器控制台截图
4. 请求和响应的详细信息
## 更新时间
2026-04-09

159
models/cms_article.go Normal file
View File

@ -0,0 +1,159 @@
package models
import (
"sync"
"time"
"github.com/beego/beego/v2/client/orm"
)
// CmsArticleCategory CMS 文章分类 yz_cms_article_category
type CmsArticleCategory struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
Cid uint64 `orm:"column(cid);default(0)" json:"cid"`
Name string `orm:"column(name);size(100)" json:"name"`
Image string `orm:"column(image);size(500);default()" json:"image"`
Desc string `orm:"column(desc);size(500);default()" json:"desc"`
Sort int `orm:"column(sort);default(0)" json:"sort"`
Status int8 `orm:"column(status);default(1)" json:"status"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *CmsArticleCategory) TableName() string {
return "yz_cms_article_category"
}
// CmsArticle CMS 文章 yz_cms_article
type CmsArticle struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
Title string `orm:"column(title);size(255)" json:"title"`
Author string `orm:"column(author);size(100);default()" json:"author"`
CateID uint64 `orm:"column(cate_id);default(0)" json:"cate_id"`
Content string `orm:"column(content);type(mediumtext);null" json:"content"`
Desc string `orm:"column(desc);size(500);default()" json:"desc"`
Image string `orm:"column(image);size(500);default()" json:"image"`
IsTrans int8 `orm:"column(is_trans);default(0)" json:"is_trans"`
TransURL *string `orm:"column(transurl);size(500);null" json:"transurl"`
Status int8 `orm:"column(status);default(0)" json:"status"`
Top int8 `orm:"column(top);default(0)" json:"top"`
Recommend int8 `orm:"column(recommend);default(0)" json:"recommend"`
Views int `orm:"column(views);default(0)" json:"views"`
Likes int `orm:"column(likes);default(0)" json:"likes"`
PublisherID *uint64 `orm:"column(publisher_id);null" json:"publisher_id"`
PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *CmsArticle) TableName() string {
return "yz_cms_article"
}
var cmsArticleTablesOnce sync.Once
// EnsureCmsArticleTables 首次使用时自动建表(若不存在)。
func EnsureCmsArticleTables() error {
var err error
cmsArticleTablesOnce.Do(func() {
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_cms_article_category (
id bigint unsigned NOT NULL AUTO_INCREMENT,
tid bigint unsigned NOT NULL DEFAULT 0,
cid bigint unsigned NOT NULL DEFAULT 0,
name varchar(100) NOT NULL DEFAULT '',
image varchar(500) NOT NULL DEFAULT '',
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
sort int NOT NULL DEFAULT 0,
status tinyint NOT NULL DEFAULT 1,
create_time datetime NOT NULL,
update_time datetime DEFAULT NULL,
delete_time datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_tid_cid (tid, cid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
if err != nil {
return
}
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_cms_article (
id bigint unsigned NOT NULL AUTO_INCREMENT,
tid bigint unsigned NOT NULL DEFAULT 0,
title varchar(255) NOT NULL DEFAULT '',
author varchar(100) NOT NULL DEFAULT '',
cate_id bigint unsigned NOT NULL DEFAULT 0,
content mediumtext,
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
image varchar(500) NOT NULL DEFAULT '',
is_trans tinyint NOT NULL DEFAULT 0,
transurl varchar(500) DEFAULT NULL,
status tinyint NOT NULL DEFAULT 0,
top tinyint NOT NULL DEFAULT 0,
recommend tinyint NOT NULL DEFAULT 0,
views int NOT NULL DEFAULT 0,
likes int NOT NULL DEFAULT 0,
publisher_id bigint unsigned DEFAULT NULL,
publish_time datetime DEFAULT NULL,
create_time datetime NOT NULL,
update_time datetime DEFAULT NULL,
delete_time datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_tid_status (tid, status),
KEY idx_cate_id (cate_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec()
})
return err
}
func CmsCategoryNameMap(tid uint64, ids []uint64) map[uint64]string {
out := make(map[uint64]string)
if len(ids) == 0 {
return out
}
var rows []CmsArticleCategory
_, _ = Orm.QueryTable(new(CmsArticleCategory)).
Filter("tid", tid).
Filter("id__in", ids).
Filter("delete_time__isnull", true).
All(&rows, "ID", "Name")
for _, r := range rows {
out[r.ID] = r.Name
}
return out
}
func CmsFormatTime(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("2006-01-02 15:04:05")
}
func CmsSimilarArticles(tid uint64, title string, limit int) ([]orm.Params, error) {
if limit <= 0 {
limit = 5
}
var rows []CmsArticle
_, err := Orm.QueryTable(new(CmsArticle)).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Filter("title__icontains", title).
Limit(limit).
All(&rows, "ID", "Title")
if err != nil {
return nil, err
}
out := make([]orm.Params, 0, len(rows))
for _, r := range rows {
out = append(out, orm.Params{
"id": r.ID,
"title": r.Title,
"similarity": 80,
})
}
return out, nil
}

View File

@ -59,6 +59,8 @@ func Init(_ string) {
new(PlatformAccountPoolKiro), new(PlatformAccountPoolKiro),
new(PlatformAccountPoolWindsurf), new(PlatformAccountPoolWindsurf),
new(PlatformAccountPoolCursor), new(PlatformAccountPoolCursor),
new(CmsArticleCategory),
new(CmsArticle),
) )
// 创建全局 Ormer // 创建全局 Ormer

View File

@ -16,5 +16,5 @@ type SystemTenantDomain struct {
} }
func (m *SystemTenantDomain) TableName() string { func (m *SystemTenantDomain) TableName() string {
return "yz_tenant_domain" return "yz_system_tenant_domain"
} }

View File

@ -1,9 +1,8 @@
// Package tokenprobe 使用号池内 Token 调用各厂商接口做可用性探测。
// Cursor 走 api2.cursor.sh 的 Connect + protobuf 二进制流(非 JSON 文本接口)。
package tokenprobe package tokenprobe
import ( import (
"bytes" "bytes"
"crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -14,9 +13,13 @@ import (
"time" "time"
) )
var httpClient = &http.Client{Timeout: 25 * time.Second} var httpClient = &http.Client{
Timeout: 12 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Result 探测结果Cursor 会填充 ProbeMessage / Endpoint / BytesRead / RawPreview 等)
type Result struct { type Result struct {
OK bool `json:"ok"` OK bool `json:"ok"`
Detail string `json:"detail"` Detail string `json:"detail"`
@ -26,12 +29,10 @@ type Result struct {
BytesRead int `json:"bytesRead,omitempty"` BytesRead int `json:"bytesRead,omitempty"`
RawPreview string `json:"rawPreview,omitempty"` RawPreview string `json:"rawPreview,omitempty"`
RequestBodyPrefixHex string `json:"requestBodyPrefixHex,omitempty"` RequestBodyPrefixHex string `json:"requestBodyPrefixHex,omitempty"`
// StreamProtocol / StreamNote 仅 Cursor Agent 探测填充,说明二进制流与结论边界 StreamProtocol string `json:"streamProtocol,omitempty"`
StreamProtocol string `json:"streamProtocol,omitempty"` StreamNote string `json:"streamNote,omitempty"`
StreamNote string `json:"streamNote,omitempty"`
} }
// ProbeOfficial 按号池模块探测 Tokencursor / windsurf / krio
func ProbeOfficial(module, rawToken string) Result { func ProbeOfficial(module, rawToken string) Result {
tok := normalizeBearerToken(strings.TrimSpace(rawToken)) tok := normalizeBearerToken(strings.TrimSpace(rawToken))
if tok == "" { if tok == "" {
@ -58,18 +59,78 @@ func normalizeBearerToken(s string) string {
} }
func probeCursor(token string) Result { func probeCursor(token string) Result {
return probeCursorHiAgent(token) url := "https://api2.cursor.sh/auth/full_stripe_profile"
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return Result{OK: false, Detail: "构造请求失败: " + err.Error()}
}
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token))
req.Header.Set("X-Cursor-Client-Version", "3.0.16")
req.Header.Set("X-New-Onboarding-Completed", "false")
req.Header.Set("X-Ghost-Mode", "true")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/3.0.16 Chrome/142.0.7444.265 Electron/39.8.1 Safari/537.36")
req.Header.Set("Accept", "*/*")
req.Header.Set("Origin", "vscode-file://vscode-app")
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
req.Header.Set("Accept-Language", "zh-CN")
req.Header.Set("Priority", "u=1, i")
resp, err := httpClient.Do(req)
if err != nil {
return Result{OK: false, Detail: "请求官方账单接口超时/网络失败: " + err.Error(), HTTPStatus: 500}
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 16384))
jsonStr := string(body)
res := Result{
HTTPStatus: resp.StatusCode,
Endpoint: url,
BytesRead: len(body),
RawPreview: jsonStr,
StreamProtocol: "HTTP/2 JSON REST",
StreamNote: "2026型画像探测",
ProbeMessage: "GET full_stripe_profile",
}
if resp.StatusCode != http.StatusOK {
res.OK = false
res.Detail = fmt.Sprintf("Token已失效或被官方拉黑HTTP %d", resp.StatusCode)
return res
}
if strings.Contains(jsonStr, `"noModelsRemaining":true`) ||
strings.Contains(jsonStr, `"is_usage_limited":true`) ||
strings.Contains(jsonStr, `"hard_limit_reached"`) ||
strings.Contains(jsonStr, `"blocked"`) ||
(strings.Contains(jsonStr, `"membershipType":"free"`) && strings.Contains(jsonStr, `"trialEligible":false`)) {
res.OK = false
res.Detail = "Token存活但属于无额度Free空壳号上号必弹付费墙"
return res
}
if len(jsonStr) < 10 {
res.OK = false
res.Detail = "官方接口返回异常空数据"
return res
}
res.OK = true
res.Detail = "检测成功,高速算力/Agent额度健康"
return res
} }
func probeWindsurf(apiKey string) Result { func probeWindsurf(apiKey string) Result {
payload := map[string]interface{}{ payload := map[string]interface{}{
"metadata": map[string]string{ "metadata": map[string]string{
"apiKey": apiKey, "apiKey": apiKey,
"ideName": "windsurf", "ideName": "windsurf",
"ideVersion": "0.0.0", "ideVersion": "0.0.0",
"extensionName": "windsurf", "extensionName": "windsurf",
"extensionVersion": "0.0.0", "extensionVersion": "0.0.0",
"locale": "zh", "locale": "zh",
}, },
} }
raw, err := json.Marshal(payload) raw, err := json.Marshal(payload)
@ -122,7 +183,7 @@ func probeKiro(accessToken string) Result {
if arn == "" { if arn == "" {
return Result{ return Result{
OK: false, OK: false,
Detail: "无法从 Token 中解析 profileArnKiro 暂无法自动探测(需完整登录 JWT", Detail: "无法从 Token 中解析 profileArnKiro 暂无法自动探测",
} }
} }

View File

@ -14,31 +14,20 @@ func Register() {
// RegisterAuthRoutes 注册 backend 认证相关路由。 // RegisterAuthRoutes 注册 backend 认证相关路由。
func RegisterAuthRoutes() { func RegisterAuthRoutes() {
// backend 登录相关(统一走 /backend/* // 登录、注册与找回密码相关
beego.Router("/backend/login", &controllers.BackendAuthController{}, "post:LoginBackend") beego.Router("/backend/login", &controllers.BackendAuthController{}, "post:LoginBackend")
beego.Router("/backend/sendLoginCode", &controllers.BackendAuthController{}, "post:SendLoginCode") beego.Router("/backend/sendLoginCode", &controllers.BackendAuthController{}, "post:SendLoginCode")
beego.Router("/backend/loginBySms", &controllers.BackendAuthController{}, "post:LoginBySms") beego.Router("/backend/loginBySms", &controllers.BackendAuthController{}, "post:LoginBySms")
beego.Router("/backend/logout", &controllers.BackendAuthController{}, "post:Logout") beego.Router("/backend/logout", &controllers.BackendAuthController{}, "post:Logout")
// 极验与登录验证配置
beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos")
beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos")
beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify")
// 登录相关接口
beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos")
beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos")
beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify")
// 注册与找回密码
beego.Router("/backend/register", &controllers.BackendAuthController{}, "post:Register") beego.Router("/backend/register", &controllers.BackendAuthController{}, "post:Register")
beego.Router("/backend/sendRegisterCode", &controllers.BackendAuthController{}, "post:SendRegisterCode") beego.Router("/backend/sendRegisterCode", &controllers.BackendAuthController{}, "post:SendRegisterCode")
beego.Router("/backend/resetPassword", &controllers.BackendAuthController{}, "post:ResetPassword") beego.Router("/backend/resetPassword", &controllers.BackendAuthController{}, "post:ResetPassword")
beego.Router("/backend/sendResetCode", &controllers.BackendAuthController{}, "post:SendResetCode") beego.Router("/backend/sendResetCode", &controllers.BackendAuthController{}, "post:SendResetCode")
// 租户站点设置 // 极验与登录验证配置
beego.Router("/backend/normalInfos", &controllers.BackendSiteSettingsController{}, "get:GetNormalInfos") beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos")
beego.Router("/backend/saveNormalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveNormalInfos") beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos")
beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify")
// 菜单接口 // 菜单接口
beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu") beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu")
@ -50,6 +39,18 @@ func RegisterAuthRoutes() {
beego.Router("/backend/operationLogs/:id", &controllers.BackendOperationLogController{}, "get:Detail;delete:Delete") beego.Router("/backend/operationLogs/:id", &controllers.BackendOperationLogController{}, "get:Detail;delete:Delete")
beego.Router("/backend/operationLogs/batchDelete", &controllers.BackendOperationLogController{}, "post:BatchDelete") beego.Router("/backend/operationLogs/batchDelete", &controllers.BackendOperationLogController{}, "post:BatchDelete")
// 租户站点设置
beego.Router("/backend/normalInfos", &controllers.BackendSiteSettingsController{}, "get:GetNormalInfos")
beego.Router("/backend/saveNormalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveNormalInfos")
beego.Router("/backend/legalInfos", &controllers.BackendSiteSettingsController{}, "get:GetLegalInfos")
beego.Router("/backend/saveLegalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveLegalInfos")
beego.Router("/backend/companyInfos", &controllers.BackendSiteSettingsController{}, "get:GetCompanyInfos")
beego.Router("/backend/saveCompanyInfos", &controllers.BackendSiteSettingsController{}, "post:SaveCompanyInfos")
beego.Router("/backend/companySeo", &controllers.BackendSiteSettingsController{}, "get:GetCompanySeo")
beego.Router("/backend/saveCompanySeo", &controllers.BackendSiteSettingsController{}, "post:SaveCompanySeo")
beego.Router("/backend/loginVerifyInfos", &controllers.BackendLoginVerifyController{}, "get:GetLoginVerifyInfos")
beego.Router("/backend/saveloginVerifyInfos", &controllers.BackendLoginVerifyController{}, "post:SaveLoginVerifyInfos")
// 文件管理yz_system_files / yz_system_files_category // 文件管理yz_system_files / yz_system_files_category
beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate") beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate")
beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles") beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles")
@ -101,6 +102,40 @@ func RegisterAuthRoutes() {
beego.Router("/backend/erp/editPosition/:id", &controllers.BackendErpController{}, "post:EditPosition") beego.Router("/backend/erp/editPosition/:id", &controllers.BackendErpController{}, "post:EditPosition")
beego.Router("/backend/erp/deletePosition/:id", &controllers.BackendErpController{}, "delete:DeletePosition") beego.Router("/backend/erp/deletePosition/:id", &controllers.BackendErpController{}, "delete:DeletePosition")
// 文章管理相关接口 // 文章管理
beego.Router("/backend/articlesList", &controllers.BackendArticleController{}, "get:List")
beego.Router("/backend/allarticles", &controllers.BackendArticleController{}, "get:ListAll")
beego.Router("/backend/articles/:id", &controllers.BackendArticleController{}, "get:Detail")
beego.Router("/backend/createarticle", &controllers.BackendArticleController{}, "post:Create")
beego.Router("/backend/editarticle/:id", &controllers.BackendArticleController{}, "post:Update")
beego.Router("/backend/deletearticle/:id", &controllers.BackendArticleController{}, "delete:Delete")
beego.Router("/backend/publisharticle/:id", &controllers.BackendArticleController{}, "post:Publish")
beego.Router("/backend/unPublisharticle/:id", &controllers.BackendArticleController{}, "post:Unpublish")
beego.Router("/backend/articleRecommend/:id", &controllers.BackendArticleController{}, "post:Recommend")
beego.Router("/backend/unArticleRecommend/:id", &controllers.BackendArticleController{}, "post:Unrecommend")
beego.Router("/backend/articleTop/:id", &controllers.BackendArticleController{}, "post:Top")
beego.Router("/backend/unArticleTop/:id", &controllers.BackendArticleController{}, "post:Untop")
beego.Router("/backend/categories", &controllers.BackendArticleCategoryController{}, "get:List")
beego.Router("/backend/allcategories", &controllers.BackendArticleCategoryController{}, "get:ListAll")
beego.Router("/backend/categories/:id", &controllers.BackendArticleCategoryController{}, "get:Detail;delete:Delete")
beego.Router("/backend/createCategory", &controllers.BackendArticleCategoryController{}, "post:Create")
beego.Router("/backend/editCategory/:id", &controllers.BackendArticleCategoryController{}, "post:Update")
beego.Router("/backend/categories/:id/status", &controllers.BackendArticleCategoryController{}, "patch:UpdateStatus")
// 域名管理(主域名池 / 租户域名)
beego.Router("/backend/domain/pool/index", &controllers.BackendDomainPoolController{}, "get:Index")
beego.Router("/backend/domain/pool/getEnabledDomains", &controllers.BackendDomainPoolController{}, "get:GetEnabledDomains")
beego.Router("/backend/domain/pool/create", &controllers.BackendDomainPoolController{}, "post:Create")
beego.Router("/backend/domain/pool/update", &controllers.BackendDomainPoolController{}, "post:Update")
beego.Router("/backend/domain/pool/delete/:id", &controllers.BackendDomainPoolController{}, "delete:Delete")
beego.Router("/backend/domain/pool/toggleStatus", &controllers.BackendDomainPoolController{}, "post:ToggleStatus")
beego.Router("/backend/domain/tenant/index", &controllers.BackendTenantDomainController{}, "get:Index")
beego.Router("/backend/domain/tenant/myDomains", &controllers.BackendTenantDomainController{}, "get:MyDomains")
beego.Router("/backend/domain/tenant/apply", &controllers.BackendTenantDomainController{}, "post:Apply")
beego.Router("/backend/domain/tenant/audit", &controllers.BackendTenantDomainController{}, "post:Audit")
beego.Router("/backend/domain/tenant/toggleStatus", &controllers.BackendTenantDomainController{}, "post:ToggleStatus")
beego.Router("/backend/domain/tenant/delete/:id", &controllers.BackendTenantDomainController{}, "delete:Delete")
} }