更新补号机制
This commit is contained in:
parent
e6b84aad80
commit
269fbd08ff
@ -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 根据账号类型返回格式化字符串
|
||||||
|
|||||||
954
controllers/backend_article.go
Normal file
954
controllers/backend_article.go
Normal 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()
|
||||||
|
}
|
||||||
600
controllers/backend_domain.go
Normal file
600
controllers/backend_domain.go
Normal 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
|
||||||
21
controllers/domain_common.go
Normal file
21
controllers/domain_common.go
Normal 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]$`)
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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
63
controllers/pool_probe.go
Normal 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 探测 Token;cursor 模块会回写 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
|
||||||
|
}
|
||||||
@ -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配置
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**部署完成后,请在此签名确认:**
|
|
||||||
|
|
||||||
- 部署人员:__________
|
|
||||||
- 部署时间:__________
|
|
||||||
- 测试人员:__________
|
|
||||||
- 测试时间:__________
|
|
||||||
- 审核人员:__________
|
|
||||||
- 审核时间:__________
|
|
||||||
@ -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`
|
|
||||||
@ -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/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🎉 恭喜!你已经完成了存储配置功能的快速开始!**
|
|
||||||
@ -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`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**所有功能已完整实现并测试通过!** 🎉
|
|
||||||
@ -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接口正常
|
|
||||||
- [ ] 前端页面显示正常
|
|
||||||
- [ ] 本地存储配置保存成功
|
|
||||||
- [ ] 七牛云配置保存成功
|
|
||||||
- [ ] 表单验证正常工作
|
|
||||||
- [ ] 配置切换功能正常
|
|
||||||
- [ ] 数据持久化正常
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
26
docs/接口文件.md
26
docs/接口文件.md
@ -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`,后续按实际需求逐步补全。
|
|
||||||
|
|
||||||
@ -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
159
models/cms_article.go
Normal 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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 按号池模块探测 Token(cursor / 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 中解析 profileArn,Kiro 暂无法自动探测(需完整登录 JWT)",
|
Detail: "无法从 Token 中解析 profileArn,Kiro 暂无法自动探测",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user