更新检测流程
This commit is contained in:
parent
81f5039458
commit
761a5cb69c
@ -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,36 +100,14 @@ 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 {
|
||||||
if err == orm.ErrNoRows {
|
return 0, nil, nil, "", "", nil, err
|
||||||
c.cardErr(404, 404, "暂无可用卡密")
|
|
||||||
} else {
|
|
||||||
c.cardErr(500, 500, "查询失败")
|
|
||||||
}
|
}
|
||||||
return
|
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, row.IsUsed, nil
|
||||||
}
|
|
||||||
|
|
||||||
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cursor 号池需要先判断可用状态:is_used=1 才发送给前端;
|
|
||||||
// is_used=0(已用完/不可用)或 NULL(未探测)则继续提取下一条。
|
|
||||||
if row.IsUsed != nil && *row.IsUsed == 1 {
|
|
||||||
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApiGetCardController) extractWindsurf(platform, dataType string, now time.Time) {
|
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
|
var row models.PlatformAccountPoolWindsurf
|
||||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
||||||
Filter("is_extracted", 0).
|
Filter("is_extracted", 0).
|
||||||
@ -138,28 +116,14 @@ func (c *ApiGetCardController) extractWindsurf(platform, dataType string, now ti
|
|||||||
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 {
|
||||||
if err == orm.ErrNoRows {
|
return 0, nil, nil, "", "", nil, err
|
||||||
c.cardErr(404, 404, "暂无可用卡密")
|
|
||||||
} else {
|
|
||||||
c.cardErr(500, 500, "查询失败")
|
|
||||||
}
|
}
|
||||||
return
|
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil
|
||||||
}
|
|
||||||
_, 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) {
|
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
|
var row models.PlatformAccountPoolKiro
|
||||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
|
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
|
||||||
Filter("is_extracted", 0).
|
Filter("is_extracted", 0).
|
||||||
@ -168,6 +132,23 @@ func (c *ApiGetCardController) extractKrio(platform, dataType string, now time.T
|
|||||||
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, 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 {
|
||||||
@ -175,18 +156,41 @@ func (c *ApiGetCardController) extractKrio(platform, dataType string, now time.T
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
|
|
||||||
Filter("id", row.ID).
|
tableName := poolTableName(module)
|
||||||
|
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
|
||||||
}
|
}
|
||||||
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType))
|
|
||||||
|
// 已有探测结论:可用则直接返回,不可用则继续下一条。
|
||||||
|
if known, available := poolIsUsedAvailable(isUsed); known {
|
||||||
|
if available {
|
||||||
|
c.cardOK(buildCardResult(account, password, token, rowDataType))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !poolProbeToken(module, rowDataType, token, id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cardOK(buildCardResult(account, password, token, rowDataType))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildCardResult 根据账号类型返回格式化字符串
|
// buildCardResult 根据账号类型返回格式化字符串
|
||||||
|
|||||||
954
go/controllers/backend_article.go
Normal file
954
go/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
go/controllers/backend_domain.go
Normal file
600
go/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
go/controllers/domain_common.go
Normal file
21
go/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]$`)
|
||||||
@ -621,24 +621,70 @@ func replenishPoolRow(c *beego.Controller, module string) {
|
|||||||
|
|
||||||
switch module {
|
switch module {
|
||||||
case "cursor":
|
case "cursor":
|
||||||
|
checkedCount := 0
|
||||||
|
unavailableCount := 0
|
||||||
|
|
||||||
|
for {
|
||||||
var row models.PlatformAccountPoolCursor
|
var row models.PlatformAccountPoolCursor
|
||||||
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||||
Filter("is_extracted", 0).Filter("data_type", payload.Type).
|
Filter("is_extracted", 0).Filter("data_type", payload.Type).
|
||||||
OrderBy("id").One(&row); err != nil {
|
OrderBy("id").One(&row); err != nil {
|
||||||
poolJSONErr(c, 404, 404, "暂无可用账号")
|
msg := "暂无可用账号"
|
||||||
|
if checkedCount > 0 {
|
||||||
|
msg = fmt.Sprintf("已检测%d个账号,其中%d个不可用,暂无可用账号", checkedCount, unavailableCount)
|
||||||
|
}
|
||||||
|
poolJSONErr(c, 404, 404, msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
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,
|
checkedCount++
|
||||||
|
isAvailable := poolProbeToken("cursor", row.DataType, row.Token, row.ID)
|
||||||
|
if !isAvailable {
|
||||||
|
unavailableCount++
|
||||||
|
if _, err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", row.ID).Update(map[string]interface{}{
|
||||||
|
// 补号流程检测出来不可用/已用完的号,仍然归类为“补号”记录。
|
||||||
|
// 不要写成已提取/已用完状态;只有接口提取后再标记不可用的号才归到已提取侧。
|
||||||
|
"is_extracted": int8(2),
|
||||||
|
"is_used": int8(0),
|
||||||
|
"extracted_time": now,
|
||||||
|
"extracted_platform": platform,
|
||||||
|
"remark": remark,
|
||||||
|
"update_time": now,
|
||||||
|
}); err != nil {
|
||||||
|
poolJSONErr(c, 500, 500, "补号检测失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", row.ID).Update(map[string]interface{}{
|
||||||
|
"is_extracted": int8(2),
|
||||||
|
"is_used": int8(1),
|
||||||
|
"extracted_time": now,
|
||||||
|
"extracted_platform": platform,
|
||||||
|
"remark": remark,
|
||||||
|
"update_time": now,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
poolJSONErr(c, 500, 500, "补号失败: "+err.Error())
|
poolJSONErr(c, 500, 500, "补号失败: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
row.IsExtracted = 2
|
row.IsExtracted = 2
|
||||||
|
isUsed := int8(1)
|
||||||
|
row.IsUsed = &isUsed
|
||||||
row.ExtractedTime = &now
|
row.ExtractedTime = &now
|
||||||
row.ExtractedPlatform = &platform
|
row.ExtractedPlatform = &platform
|
||||||
row.Remark = remark
|
row.Remark = remark
|
||||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "补号成功", "data": row}
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "补号成功",
|
||||||
|
"data": row,
|
||||||
|
"probe": map[string]interface{}{
|
||||||
|
"checkedCount": checkedCount,
|
||||||
|
"unavailableCount": unavailableCount,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
case "windsurf":
|
case "windsurf":
|
||||||
var row models.PlatformAccountPoolWindsurf
|
var row models.PlatformAccountPoolWindsurf
|
||||||
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
||||||
@ -973,12 +1019,12 @@ func probePoolToken(c *beego.Controller, module string) {
|
|||||||
if r.StreamNote != "" {
|
if r.StreamNote != "" {
|
||||||
data["streamNote"] = r.StreamNote
|
data["streamNote"] = r.StreamNote
|
||||||
}
|
}
|
||||||
|
// Cursor 探测状态只按底层探针结论 r.OK 保存。
|
||||||
|
// 注意:客户端版本过旧只是 warning,Token 仍可用时 r.OK=true,不能因此写成已用完。
|
||||||
if module == "cursor" && payload.ID > 0 && r.HTTPStatus == http.StatusOK {
|
if module == "cursor" && payload.ID > 0 && r.HTTPStatus == http.StatusOK {
|
||||||
var isUsed int8
|
isUsed := int8(0)
|
||||||
if r.OK {
|
if r.OK {
|
||||||
isUsed = 1
|
isUsed = 1
|
||||||
} else {
|
|
||||||
isUsed = 0
|
|
||||||
}
|
}
|
||||||
if _, uerr := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", payload.ID).Update(orm.Params{
|
if _, uerr := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", payload.ID).Update(orm.Params{
|
||||||
"is_used": isUsed,
|
"is_used": isUsed,
|
||||||
@ -995,6 +1041,62 @@ func probePoolToken(c *beego.Controller, module string) {
|
|||||||
_ = c.ServeJSON()
|
_ = c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func poolTableName(module string) string {
|
||||||
|
switch module {
|
||||||
|
case "cursor":
|
||||||
|
return (&models.PlatformAccountPoolCursor{}).TableName()
|
||||||
|
case "windsurf":
|
||||||
|
return (&models.PlatformAccountPoolWindsurf{}).TableName()
|
||||||
|
case "krio":
|
||||||
|
return (&models.PlatformAccountPoolKiro{}).TableName()
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func poolIsUsedAvailable(isUsed *int8) (known bool, available bool) {
|
||||||
|
if isUsed == nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
switch *isUsed {
|
||||||
|
case 1:
|
||||||
|
return true, true
|
||||||
|
case 0:
|
||||||
|
return true, false
|
||||||
|
default:
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func poolProbeToken(module, rowDataType, token string, id uint64) bool {
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if rowDataType == "account" || token == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
r := tokenprobe.ProbeOfficial(module, token)
|
||||||
|
|
||||||
|
// Cursor 自动探测只按底层探针结论 r.OK 判定。
|
||||||
|
// 客户端版本过旧是 warning,不代表 Token 已用完;只有 tokenprobe 明确判定额度用尽/不可用时 r.OK 才为 false。
|
||||||
|
available := r.OK
|
||||||
|
|
||||||
|
// 更新数据库中的 is_used 字段
|
||||||
|
if module == "cursor" && id > 0 {
|
||||||
|
isUsed := int8(0)
|
||||||
|
if available {
|
||||||
|
isUsed = 1
|
||||||
|
}
|
||||||
|
_, _ = models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||||
|
Filter("id", id).
|
||||||
|
Update(orm.Params{
|
||||||
|
"is_used": isUsed,
|
||||||
|
"update_time": time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
func (c *PlatformAccountPoolCursorController) List() { listPoolRows(&c.Controller, "cursor") }
|
func (c *PlatformAccountPoolCursorController) List() { listPoolRows(&c.Controller, "cursor") }
|
||||||
func (c *PlatformAccountPoolCursorController) Add() { addPoolRow(&c.Controller, "cursor") }
|
func (c *PlatformAccountPoolCursorController) Add() { addPoolRow(&c.Controller, "cursor") }
|
||||||
func (c *PlatformAccountPoolCursorController) BatchAdd() { batchAddPoolRows(&c.Controller, "cursor") }
|
func (c *PlatformAccountPoolCursorController) BatchAdd() { batchAddPoolRows(&c.Controller, "cursor") }
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
192
go/models/cms_article.go
Normal file
192
go/models/cms_article.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beego/beego/v2/client/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmsArticle CMS文章表: yz_cms_article
|
||||||
|
type CmsArticle struct {
|
||||||
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
|
Tid uint64 `orm:"column(tid)" 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(text);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(1)" json:"status"` // 1草稿 2已发布 3已下架
|
||||||
|
Views int64 `orm:"column(views);default(0)" json:"views"`
|
||||||
|
Likes int64 `orm:"column(likes);default(0)" json:"likes"`
|
||||||
|
Top int8 `orm:"column(top);default(0)" json:"top"`
|
||||||
|
Recommend int8 `orm:"column(recommend);default(0)" json:"recommend"`
|
||||||
|
PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"`
|
||||||
|
PublisherID uint64 `orm:"column(publisher_id);default(0)" json:"publisher_id"`
|
||||||
|
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||||
|
UpdateTime *time.Time `orm:"column(update_time);type(datetime);auto_now;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"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmsArticleCategory CMS文章分类表: yz_cms_article_category
|
||||||
|
type CmsArticleCategory struct {
|
||||||
|
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||||
|
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||||
|
Cid uint64 `orm:"column(cid);default(0)" json:"cid"` // 父级分类ID
|
||||||
|
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);type(datetime);auto_now_add" json:"create_time"`
|
||||||
|
UpdateTime *time.Time `orm:"column(update_time);type(datetime);auto_now;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"
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureCmsArticleTables 确保 CMS 文章相关表存在。
|
||||||
|
func EnsureCmsArticleTables() error {
|
||||||
|
sqls := []string{
|
||||||
|
`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 DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
` + "`update_time`" + ` datetime NULL DEFAULT NULL,
|
||||||
|
` + "`delete_time`" + ` datetime NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (` + "`id`" + `),
|
||||||
|
KEY ` + "`idx_tid`" + ` (` + "`tid`" + `),
|
||||||
|
KEY ` + "`idx_tid_cid`" + ` (` + "`tid`" + `,` + "`cid`" + `),
|
||||||
|
KEY ` + "`idx_delete_time`" + ` (` + "`delete_time`" + `)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||||
|
`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`" + ` longtext NULL,
|
||||||
|
` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '',
|
||||||
|
` + "`image`" + ` varchar(500) NOT NULL DEFAULT '',
|
||||||
|
` + "`is_trans`" + ` tinyint NOT NULL DEFAULT 0,
|
||||||
|
` + "`transurl`" + ` varchar(500) NULL DEFAULT NULL,
|
||||||
|
` + "`status`" + ` tinyint NOT NULL DEFAULT 1,
|
||||||
|
` + "`views`" + ` bigint NOT NULL DEFAULT 0,
|
||||||
|
` + "`likes`" + ` bigint NOT NULL DEFAULT 0,
|
||||||
|
` + "`top`" + ` tinyint NOT NULL DEFAULT 0,
|
||||||
|
` + "`recommend`" + ` tinyint NOT NULL DEFAULT 0,
|
||||||
|
` + "`publish_time`" + ` datetime NULL DEFAULT NULL,
|
||||||
|
` + "`publisher_id`" + ` bigint unsigned NOT NULL DEFAULT 0,
|
||||||
|
` + "`create_time`" + ` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
` + "`update_time`" + ` datetime NULL DEFAULT NULL,
|
||||||
|
` + "`delete_time`" + ` datetime NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (` + "`id`" + `),
|
||||||
|
KEY ` + "`idx_tid`" + ` (` + "`tid`" + `),
|
||||||
|
KEY ` + "`idx_tid_cate`" + ` (` + "`tid`" + `,` + "`cate_id`" + `),
|
||||||
|
KEY ` + "`idx_tid_status`" + ` (` + "`tid`" + `,` + "`status`" + `),
|
||||||
|
KEY ` + "`idx_delete_time`" + ` (` + "`delete_time`" + `)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sqlStr := range sqls {
|
||||||
|
if _, err := Orm.Raw(sqlStr).Exec(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmsFormatTime 格式化 CMS 可空时间。
|
||||||
|
func CmsFormatTime(t *time.Time) string {
|
||||||
|
if t == nil || t.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmsCategoryNameMap 批量获取分类ID到分类名称的映射。
|
||||||
|
func CmsCategoryNameMap(tid uint64, cateIDs []uint64) map[uint64]string {
|
||||||
|
result := make(map[uint64]string)
|
||||||
|
if tid == 0 || len(cateIDs) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[uint64]bool)
|
||||||
|
ids := make([]uint64, 0, len(cateIDs))
|
||||||
|
for _, id := range cateIDs {
|
||||||
|
if id == 0 || seen[id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = true
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []CmsArticleCategory
|
||||||
|
_, err := Orm.QueryTable(new(CmsArticleCategory)).
|
||||||
|
Filter("tid", tid).
|
||||||
|
Filter("id__in", ids).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
All(&rows, "id", "name")
|
||||||
|
if err != nil && err != orm.ErrNoRows {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.ID] = row.Name
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmsSimilarArticles 根据标题查找相似文章,用于创建文章时提示重复内容。
|
||||||
|
func CmsSimilarArticles(tid uint64, title string, limit int) ([]map[string]interface{}, error) {
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
if tid == 0 || title == "" {
|
||||||
|
return []map[string]interface{}{}, nil
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []CmsArticle
|
||||||
|
_, err := Orm.QueryTable(new(CmsArticle)).
|
||||||
|
Filter("tid", tid).
|
||||||
|
Filter("title__icontains", title).
|
||||||
|
Filter("delete_time__isnull", true).
|
||||||
|
OrderBy("-id").
|
||||||
|
Limit(limit).
|
||||||
|
All(&rows, "id", "title", "cate_id", "status", "create_time")
|
||||||
|
if err != nil && err != orm.ErrNoRows {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]map[string]interface{}, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, map[string]interface{}{
|
||||||
|
"id": row.ID,
|
||||||
|
"title": row.Title,
|
||||||
|
"cate_id": row.CateID,
|
||||||
|
"status": row.Status,
|
||||||
|
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||||
|
"similarity": fmt.Sprintf("标题包含“%s”", title),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@ -56,6 +56,8 @@ func Init(_ string) {
|
|||||||
new(ComplaintCategory),
|
new(ComplaintCategory),
|
||||||
new(PlatformComplaint),
|
new(PlatformComplaint),
|
||||||
new(SystemSoftwareUpgrade),
|
new(SystemSoftwareUpgrade),
|
||||||
|
new(CmsArticle),
|
||||||
|
new(CmsArticleCategory),
|
||||||
new(PlatformAccountPoolKiro),
|
new(PlatformAccountPoolKiro),
|
||||||
new(PlatformAccountPoolWindsurf),
|
new(PlatformAccountPoolWindsurf),
|
||||||
new(PlatformAccountPoolCursor),
|
new(PlatformAccountPoolCursor),
|
||||||
|
|||||||
BIN
go/output.log
Normal file
BIN
go/output.log
Normal file
Binary file not shown.
@ -10,6 +10,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -22,7 +23,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
cursorBackendURL = "https://api2.cursor.sh"
|
cursorBackendURL = "https://api2.cursor.sh"
|
||||||
cursorAgentPath = "/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
|
cursorAgentPath = "/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
|
||||||
cursorClientVersion = "2.6.22"
|
cursorClientVersion = "3.6.31"
|
||||||
cursorHiMaxRead = 512 * 1024
|
cursorHiMaxRead = 512 * 1024
|
||||||
// probeHiText 发往官方 Agent 的探测内容(与前端展示 probeMessage 一致)
|
// probeHiText 发往官方 Agent 的探测内容(与前端展示 probeMessage 一致)
|
||||||
probeHiText = "hi"
|
probeHiText = "hi"
|
||||||
@ -276,55 +277,43 @@ var cursorQuotaTipSig = []byte("Get Cursor Pro for more Agent usage, unlimited T
|
|||||||
|
|
||||||
const cursorLimitTipPrefix = "Get Cursor Pro for more Agent usage, unlimited Tab"
|
const cursorLimitTipPrefix = "Get Cursor Pro for more Agent usage, unlimited Tab"
|
||||||
|
|
||||||
// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示(ASCII 区不区分大小写 + UTF-8 短语)
|
// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示
|
||||||
func classifyCursorRawStream(raw []byte) (blocked bool, reason string) {
|
// 返回 (isQuotaExhausted, message)
|
||||||
|
// isQuotaExhausted: true 表示额度用完/Token不可用,false 表示 Token 可用(可能有警告信息)
|
||||||
|
func classifyCursorRawStream(raw []byte) (isQuotaExhausted bool, message string) {
|
||||||
if len(raw) == 0 {
|
if len(raw) == 0 {
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 额度用尽只按明确完整提示判定,避免“可用但带推广/提示文案”的 Token 被误标为已用完。
|
||||||
|
// 用户自定义的二进制特征仍保留给部署方精确配置。
|
||||||
for _, sig := range cursorQuotaExhaustedSigsFromEnv() {
|
for _, sig := range cursorQuotaExhaustedSigsFromEnv() {
|
||||||
if bytes.Contains(raw, sig) {
|
if bytes.Contains(raw, sig) {
|
||||||
return true, fmt.Sprintf("流中匹配:CURSOR_QUOTA_EXHAUSTED_SIG_HEX 配置的二进制特征(%d 字节)", len(sig))
|
return true, "该TOKEN已用完(额度已耗尽)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bytes.Contains(raw, cursorQuotaTipSig) {
|
if bytes.Contains(raw, cursorQuotaTipSig) {
|
||||||
return true, "流中匹配:" + string(cursorQuotaTipSig)
|
return true, "该TOKEN已用完(Get Cursor Pro for more Agent usage, unlimited Tab, and more.)"
|
||||||
}
|
|
||||||
// 社区脚本:仅到「…Agent usage」的 ASCII 前缀(流里可能只有前半段)
|
|
||||||
if bytes.Contains(raw, cursorQuotaExhaustedSigCommunity) {
|
|
||||||
return true, "流中匹配:Get Cursor Pro for more Agent usage…(社区 QuotaExhaustedSignature 前缀)"
|
|
||||||
}
|
|
||||||
if bytes.Contains(raw, []byte(cursorLimitTipPrefix)) {
|
|
||||||
return true, "流中匹配:" + cursorLimitTipPrefix + "…"
|
|
||||||
}
|
|
||||||
low := append([]byte(nil), raw...)
|
|
||||||
asciiLowerInPlace(low)
|
|
||||||
if bytes.Contains(low, []byte("you've hit your usage limit")) ||
|
|
||||||
bytes.Contains(low, []byte("youve hit your usage limit")) ||
|
|
||||||
bytes.Contains(low, []byte("hit your usage limit")) {
|
|
||||||
return true, "流中匹配:hit your usage limit / you've hit your usage limit"
|
|
||||||
}
|
|
||||||
if bytes.Contains(low, []byte("get cursor pro for more agent usage")) {
|
|
||||||
return true, "流中匹配:get cursor pro for more agent usage"
|
|
||||||
}
|
|
||||||
if bytes.Contains(low, []byte("upgrade to pro")) {
|
|
||||||
return true, "流中匹配:upgrade to pro"
|
|
||||||
}
|
|
||||||
if bytes.Contains(low, []byte("get cursor pro")) && bytes.Contains(low, []byte("agent")) {
|
|
||||||
return true, "流中匹配:get cursor pro + agent"
|
|
||||||
}
|
|
||||||
if bytes.Contains(low, []byte("usage limit")) {
|
|
||||||
return true, "流中匹配:usage limit"
|
|
||||||
}
|
|
||||||
if bytes.Contains(low, []byte("unlimited tab")) && bytes.Contains(low, []byte("cursor pro")) {
|
|
||||||
return true, "流中匹配:unlimited tab + cursor pro"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flat := strings.ToLower(strings.ToValidUTF8(string(raw), "\uFFFD"))
|
flat := strings.ToLower(strings.ToValidUTF8(string(raw), "\uFFFD"))
|
||||||
flat = strings.ReplaceAll(flat, "\u2019", "'") // 右单引号
|
flat = strings.ReplaceAll(flat, "\u2019", "'")
|
||||||
flat = strings.ReplaceAll(flat, "`", "'")
|
flat = strings.ReplaceAll(flat, "`", "'")
|
||||||
if strings.Contains(flat, "you've hit your usage limit") {
|
|
||||||
return true, "流中匹配:you've hit your usage limit(UTF-8)"
|
if strings.Contains(flat, "suspicious activity") ||
|
||||||
|
strings.Contains(flat, "unauthenticated") ||
|
||||||
|
strings.Contains(flat, "unauthorized request") ||
|
||||||
|
strings.Contains(flat, "unauthorizedrequest") ||
|
||||||
|
strings.Contains(flat, "error_unauthorized") {
|
||||||
|
return true, "该TOKEN不可用(账号触发可疑活动风控/未认证,需要重新登录)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 版本过旧警告 - 这不是额度问题,Token 仍然可用
|
||||||
|
// 返回 false,表示 Token 可用
|
||||||
|
if strings.Contains(flat, "very old version") || strings.Contains(flat, "update to the latest version") {
|
||||||
|
return false, "Token可用,但客户端版本过旧,建议更新到最新版本"
|
||||||
|
}
|
||||||
|
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,41 +389,29 @@ func decodeConnectFramedBody(raw []byte) ([]byte, string, bool) {
|
|||||||
return nil, "", false
|
return nil, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
note := fmt.Sprintf("响应体已按 Connect 分帧解析(%d 帧", frameCount)
|
return out.Bytes(), "", true
|
||||||
if compressedFrames > 0 {
|
|
||||||
note += fmt.Sprintf(",其中 %d 帧已做 gzip 解压", compressedFrames)
|
|
||||||
}
|
|
||||||
note += ")后分析"
|
|
||||||
return out.Bytes(), note, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeCursorResponseBody(raw []byte, contentEncoding string) ([]byte, string) {
|
func decodeCursorResponseBody(raw []byte, contentEncoding string) ([]byte, string) {
|
||||||
if decoded, note, ok := decodeConnectFramedBody(raw); ok {
|
if decoded, _, ok := decodeConnectFramedBody(raw); ok {
|
||||||
return decoded, note
|
return decoded, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
enc := strings.ToLower(strings.TrimSpace(contentEncoding))
|
enc := strings.ToLower(strings.TrimSpace(contentEncoding))
|
||||||
if strings.Contains(enc, "gzip") || looksLikeGzip(raw) {
|
if strings.Contains(enc, "gzip") || looksLikeGzip(raw) {
|
||||||
decoded, err := gunzipBytes(raw)
|
decoded, err := gunzipBytes(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(enc, "gzip") {
|
return raw, ""
|
||||||
return raw, "响应头声明 gzip,但解压失败,已回退为原始字节预览"
|
|
||||||
}
|
}
|
||||||
return raw, "检测到 gzip 魔数,但解压失败,已回退为原始字节预览"
|
return decoded, ""
|
||||||
}
|
}
|
||||||
if strings.Contains(enc, "gzip") {
|
return raw, ""
|
||||||
return decoded, "响应体已按 gzip 解压后分析"
|
|
||||||
}
|
|
||||||
return decoded, "响应体虽未显式声明 Content-Encoding,但按 gzip 魔数解压后分析"
|
|
||||||
}
|
|
||||||
if enc != "" {
|
|
||||||
return raw, "响应头 Content-Encoding=" + enc + ",当前未额外解码,按原始字节分析"
|
|
||||||
}
|
|
||||||
return raw, "响应体未压缩或未声明压缩,且未识别为 Connect 分帧,按原始字节分析"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cursorStreamProtocol 与官方客户端一致:Connect-RPC + protobuf 体,HTTP/2 流式
|
// cursorStreamProtocol 与官方客户端一致:Connect-RPC + protobuf 体,HTTP/2 流式。
|
||||||
const cursorStreamProtocol = "Connect-Protocol-Version:1 + application/connect+proto,HTTP/2 二进制流(gRPC 兼容形态,非 JSON REST)"
|
// 当前探测接口使用新版 Agent:/aiserver.v1.ChatService/StreamUnifiedChatWithTools。
|
||||||
|
// 若 Cursor 后续强制更高客户端版本,可通过环境变量 CURSOR_CLIENT_VERSION 覆盖默认 X-Cursor-Client-Version。
|
||||||
|
const cursorStreamProtocol = "Connect-Protocol-Version:1 + application/connect+proto,HTTP/2 二进制流(gRPC/ConnectRPC 兼容形态,非 JSON REST)"
|
||||||
|
|
||||||
// cursorStreamNote 说明 rawPreview / ok 的含义边界(与「仅通 200」结论一致)
|
// cursorStreamNote 说明 rawPreview / ok 的含义边界(与「仅通 200」结论一致)
|
||||||
const cursorStreamNote = `【协议】本 URL 为 Cursor 官方 Agent 流式接口,请求体为 protobuf(requestBodyPrefixHex 可见非表单/JSON)。` +
|
const cursorStreamNote = `【协议】本 URL 为 Cursor 官方 Agent 流式接口,请求体为 protobuf(requestBodyPrefixHex 可见非表单/JSON)。` +
|
||||||
@ -484,6 +461,189 @@ func cursorProbeResult(ok bool, detail string, httpStatus int, reqBody, raw, pre
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cursorReadableServerOutput 从 Cursor 的 protobuf 二进制流里提取适合展示的可读文本。
|
||||||
|
// 注意:这里不完整解析 proto,只做展示层清洗,避免把字段号、长度前缀、UUID、think 过程等内容直接展示给用户。
|
||||||
|
func cursorReadableServerOutput(decoded []byte, maxBytes int) string {
|
||||||
|
if len(decoded) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
s := strings.ToValidUTF8(string(decoded), "")
|
||||||
|
s = strings.ReplaceAll(s, "\uFFFD", "")
|
||||||
|
finalMarkerRe := regexp.MustCompile(`(?is)<\s*[||]\s*final\s*[||]\s*>`)
|
||||||
|
s = finalMarkerRe.ReplaceAllString(s, "<final>")
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
lastSpace := false
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case r == '\r' || r == '\n' || r == '\t' || r == ' ':
|
||||||
|
if !lastSpace {
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
lastSpace = true
|
||||||
|
case r >= 32:
|
||||||
|
b.WriteRune(r)
|
||||||
|
lastSpace = false
|
||||||
|
default:
|
||||||
|
// protobuf 字段号、长度前缀等控制字符经常刚好位于单词/JSON 字段之间。
|
||||||
|
// 这里用分隔符替代直接丢弃,避免 Your + request 被粘成 Yourrequest。
|
||||||
|
if !lastSpace {
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
lastSpace = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaned := strings.TrimSpace(b.String())
|
||||||
|
if cleaned == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
uuidRe := regexp.MustCompile(`(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b`)
|
||||||
|
cleaned = uuidRe.ReplaceAllString(cleaned, "")
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
const oldVersionText = "This is a very old version of Cursor. Please update to the latest version at [cursor.com/downloads](https://cursor.com/downloads)"
|
||||||
|
if strings.Contains(cleaned, oldVersionText) {
|
||||||
|
parts = append(parts, oldVersionText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先展示最终回复:先按 <final> 切分;没有 final 标记时,按被二进制流切碎的 </think> 标记切分。
|
||||||
|
finalText := ""
|
||||||
|
if idx := strings.LastIndex(cleaned, "<final>"); idx >= 0 {
|
||||||
|
finalText = cleaned[idx+len("<final>"):]
|
||||||
|
} else {
|
||||||
|
thinkCloseRe := regexp.MustCompile(`(?is)</\s*t\s*h\s*i\s*n\s*k\s*>`)
|
||||||
|
matches := thinkCloseRe.FindAllStringIndex(cleaned, -1)
|
||||||
|
if len(matches) > 0 {
|
||||||
|
finalText = cleaned[matches[len(matches)-1][1]:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(finalText) == "" {
|
||||||
|
finalText = cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
finalText = cursorJoinFragmentedText(finalText)
|
||||||
|
if errorMessage := cursorExtractReadableCursorError(finalText); errorMessage != "" {
|
||||||
|
finalText = errorMessage
|
||||||
|
}
|
||||||
|
finalText = strings.TrimSuffix(finalText, "{}")
|
||||||
|
finalText = strings.TrimSpace(finalText)
|
||||||
|
finalText = strings.Trim(finalText, `'"#%{} `)
|
||||||
|
|
||||||
|
// 清理流尾残留的二进制标记,例如:a%߯B{}
|
||||||
|
tailJunkRe := regexp.MustCompile(`(?is)\s+[a-z]?%[^\s]{0,12}B\{\}\s*$`)
|
||||||
|
finalText = tailJunkRe.ReplaceAllString(finalText, "")
|
||||||
|
finalText = strings.TrimSpace(finalText)
|
||||||
|
|
||||||
|
if finalText != "" && !strings.Contains(strings.Join(parts, "\n"), finalText) {
|
||||||
|
parts = append(parts, finalText)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) > 0 {
|
||||||
|
cleaned = strings.Join(parts, "\n\n")
|
||||||
|
} else {
|
||||||
|
cleaned = finalText
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxBytes > 0 && len(cleaned) > maxBytes {
|
||||||
|
cleaned = cleaned[:maxBytes]
|
||||||
|
for len(cleaned) > 0 && !utf8.ValidString(cleaned) {
|
||||||
|
cleaned = cleaned[:len(cleaned)-1]
|
||||||
|
}
|
||||||
|
cleaned += "…(已截断)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(cleaned)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cursorExtractReadableCursorError(text string) string {
|
||||||
|
if text == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
unescaped := strings.ReplaceAll(text, `\n`, "\n")
|
||||||
|
unescaped = strings.ReplaceAll(unescaped, `\"`, `"`)
|
||||||
|
unescaped = strings.ReplaceAll(unescaped, `\/`, `/`)
|
||||||
|
|
||||||
|
if !(strings.Contains(strings.ToLower(unescaped), "error") ||
|
||||||
|
strings.Contains(strings.ToLower(unescaped), "unauthenticated") ||
|
||||||
|
strings.Contains(strings.ToLower(unescaped), "unauthorized") ||
|
||||||
|
strings.Contains(strings.ToLower(unescaped), "suspicious activity")) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
messageRe := regexp.MustCompile(`(?is)"(?:message|detail)"\s*:\s*"([^"]+)"`)
|
||||||
|
matches := messageRe.FindAllStringSubmatch(unescaped, -1)
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg := strings.TrimSpace(match[1])
|
||||||
|
if msg == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg = strings.ReplaceAll(msg, `\n`, "\n")
|
||||||
|
msg = strings.ReplaceAll(msg, `\"`, `"`)
|
||||||
|
msg = cursorJoinFragmentedText(msg)
|
||||||
|
|
||||||
|
lowerMsg := strings.ToLower(msg)
|
||||||
|
if strings.Contains(lowerMsg, "suspicious activity") ||
|
||||||
|
strings.Contains(lowerMsg, "blocked") ||
|
||||||
|
strings.Contains(lowerMsg, "unauthorized") ||
|
||||||
|
strings.Contains(lowerMsg, "unauthenticated") {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(unescaped), "suspicious activity") {
|
||||||
|
return "Your request has been blocked as our system has detected suspicious activity from your account. For troubleshooting, please visit the Cursor Docs at https://cursor.com/docs/troubleshooting/common-issues#suspicious-activity-message."
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func cursorJoinFragmentedText(text string) string {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
parts := make([]string, 0, len(lines))
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
part := strings.TrimSpace(line)
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := strings.Join(parts, " ")
|
||||||
|
|
||||||
|
// 标点前不保留空格。
|
||||||
|
punctRe := regexp.MustCompile(`\s+([.,!?;:)\]}"',。!?;:)】》])`)
|
||||||
|
out = punctRe.ReplaceAllString(out, "$1")
|
||||||
|
|
||||||
|
// 只修复很明确的“单词内部被切开”场景,避免把 How can / I help / with your 误拼成 Howcan / Ihelp / withyour。
|
||||||
|
singlePrefixRe := regexp.MustCompile(`\b([b-hj-zB-HJ-Z])\s+([a-z]{2,})\b`)
|
||||||
|
out = singlePrefixRe.ReplaceAllString(out, "$1$2")
|
||||||
|
|
||||||
|
commonSuffixRe := regexp.MustCompile(`\b([A-Za-z]{3,})\s+(ing|ed|er|ers|ly|s)\b`)
|
||||||
|
out = commonSuffixRe.ReplaceAllString(out, "$1$2")
|
||||||
|
|
||||||
|
spaceRe := regexp.MustCompile(`\s+`)
|
||||||
|
out = spaceRe.ReplaceAllString(out, " ")
|
||||||
|
return strings.TrimSpace(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cursorServerOutputDetail 将服务器响应内容放入 detail,便于前端只展示 detail 时也能看到服务端输出。
|
||||||
|
func cursorServerOutputDetail(prefix string, decoded []byte) string {
|
||||||
|
serverOutput := cursorReadableServerOutput(decoded, 8000)
|
||||||
|
if serverOutput == "" {
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
return prefix + ",服务器可读输出:\n" + serverOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
// probeCursorHiAgent 探测 Cursor Token 可用性
|
||||||
func probeCursorHiAgent(authToken string) Result {
|
func probeCursorHiAgent(authToken string) Result {
|
||||||
if strings.Contains(authToken, "::") {
|
if strings.Contains(authToken, "::") {
|
||||||
if i := strings.LastIndex(authToken, "::"); i >= 0 {
|
if i := strings.LastIndex(authToken, "::"); i >= 0 {
|
||||||
@ -504,7 +664,7 @@ func probeCursorHiAgent(authToken string) Result {
|
|||||||
fullURL := cursorBackendURL + cursorAgentPath
|
fullURL := cursorBackendURL + cursorAgentPath
|
||||||
req, err := http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(body))
|
req, err := http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r := cursorProbeResult(false, err.Error(), 0, body, nil, nil)
|
r := cursorProbeResult(false, "请求失败: "+err.Error(), 0, body, nil, nil)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||||
@ -532,29 +692,36 @@ func probeCursorHiAgent(authToken string) Result {
|
|||||||
|
|
||||||
resp, err := cursorProbeHTTPClient.Do(req)
|
resp, err := cursorProbeHTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cursorProbeResult(false, "请求 Cursor Agent 失败: "+err.Error(), 0, body, nil, nil)
|
return cursorProbeResult(false, "请求失败: "+err.Error(), 0, body, nil, nil)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, cursorHiMaxRead))
|
raw, _ := io.ReadAll(io.LimitReader(resp.Body, cursorHiMaxRead))
|
||||||
decoded, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||||
blocked, reason := classifyCursorRawStream(decoded)
|
isQuotaExhausted, msg := classifyCursorRawStream(decoded)
|
||||||
if blocked {
|
if isQuotaExhausted {
|
||||||
return cursorProbeResult(false, reason+";"+decodeNote, resp.StatusCode, body, raw, decoded)
|
return cursorProbeResult(false, msg, resp.StatusCode, body, raw, decoded)
|
||||||
}
|
}
|
||||||
detail := fmt.Sprintf("HTTP %d(非 200);%s;说明与协议边界见 streamNote", resp.StatusCode, decodeNote)
|
// 非 200 状态码且不是额度问题
|
||||||
return cursorProbeResult(false, detail, resp.StatusCode, body, raw, decoded)
|
return cursorProbeResult(false, fmt.Sprintf("HTTP %d - Token不可用", resp.StatusCode), resp.StatusCode, body, raw, decoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
_, _ = io.Copy(&buf, io.LimitReader(resp.Body, cursorHiMaxRead))
|
_, _ = io.Copy(&buf, io.LimitReader(resp.Body, cursorHiMaxRead))
|
||||||
raw := buf.Bytes()
|
raw := buf.Bytes()
|
||||||
decoded, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||||
blocked, reason := classifyCursorRawStream(decoded)
|
isQuotaExhausted, msg := classifyCursorRawStream(decoded)
|
||||||
if blocked {
|
|
||||||
return cursorProbeResult(false, reason+";"+decodeNote, resp.StatusCode, body, raw, decoded)
|
if isQuotaExhausted {
|
||||||
|
// Token 不可用(额度用完等)
|
||||||
|
return cursorProbeResult(false, msg, resp.StatusCode, body, raw, decoded)
|
||||||
}
|
}
|
||||||
detail := "HTTP 200;未命中内置英文关键词;" + decodeNote + ";二进制流含义与 ok 边界见 streamNote"
|
|
||||||
return cursorProbeResult(true, detail, resp.StatusCode, body, raw, decoded)
|
// Token 可用时也把服务器实际输出放到 Detail,避免前端只展示 Detail 时看不到 RawPreview。
|
||||||
|
if msg != "" {
|
||||||
|
return cursorProbeResult(true, cursorServerOutputDetail(msg, decoded), resp.StatusCode, body, raw, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursorProbeResult(true, cursorServerOutputDetail("Token可用", decoded), resp.StatusCode, body, raw, decoded)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,6 @@ 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"`
|
||||||
}
|
}
|
||||||
@ -39,7 +38,8 @@ func ProbeOfficial(module, rawToken string) Result {
|
|||||||
}
|
}
|
||||||
switch module {
|
switch module {
|
||||||
case "cursor":
|
case "cursor":
|
||||||
return probeCursor(tok)
|
// 直接使用 cursor_hi.go 中已有的完整探测函数
|
||||||
|
return probeCursorHiAgent(tok)
|
||||||
case "windsurf":
|
case "windsurf":
|
||||||
return probeWindsurf(tok)
|
return probeWindsurf(tok)
|
||||||
case "krio":
|
case "krio":
|
||||||
@ -57,10 +57,12 @@ func normalizeBearerToken(s string) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// probeCursor Cursor Token 探测(直接使用 cursor_hi.go 的实现)
|
||||||
func probeCursor(token string) Result {
|
func probeCursor(token string) Result {
|
||||||
return probeCursorHiAgent(token)
|
return probeCursorHiAgent(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// probeWindsurf WindSurf 探测
|
||||||
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{
|
||||||
@ -117,6 +119,7 @@ func probeWindsurf(apiKey string) Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// probeKiro Kiro 探测
|
||||||
func probeKiro(accessToken string) Result {
|
func probeKiro(accessToken string) Result {
|
||||||
arn := findProfileArnInJWT(accessToken)
|
arn := findProfileArnInJWT(accessToken)
|
||||||
if arn == "" {
|
if arn == "" {
|
||||||
@ -160,6 +163,7 @@ func probeKiro(accessToken string) Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeJWTPayloadMap 解析 JWT payload
|
||||||
func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
|
func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
|
||||||
tok := normalizeBearerToken(strings.TrimSpace(raw))
|
tok := normalizeBearerToken(strings.TrimSpace(raw))
|
||||||
parts := strings.Split(tok, ".")
|
parts := strings.Split(tok, ".")
|
||||||
@ -177,6 +181,7 @@ func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findProfileArnInJWT 从 JWT 中查找 profileArn
|
||||||
func findProfileArnInJWT(raw string) string {
|
func findProfileArnInJWT(raw string) string {
|
||||||
m, err := decodeJWTPayloadMap(raw)
|
m, err := decodeJWTPayloadMap(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -185,6 +190,7 @@ func findProfileArnInJWT(raw string) string {
|
|||||||
return findProfileArnValue(m)
|
return findProfileArnValue(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findProfileArnValue 递归查找 profileArn
|
||||||
func findProfileArnValue(v interface{}) string {
|
func findProfileArnValue(v interface{}) string {
|
||||||
switch x := v.(type) {
|
switch x := v.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
|
|||||||
BIN
go/test.exe
Normal file
BIN
go/test.exe
Normal file
Binary file not shown.
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, watch } from 'vue';
|
import { reactive, ref, watch } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|||||||
@ -1,5 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
import {
|
||||||
|
computed,
|
||||||
|
h,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import Edit from "./components/edit.vue";
|
import Edit from "./components/edit.vue";
|
||||||
import DetailDialog from "./components/detail.vue";
|
import DetailDialog from "./components/detail.vue";
|
||||||
@ -88,7 +97,15 @@ const typeTabs = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [query.account, query.token, query.remark, query.status, query.platform, query.usable, activeTypeTab.value],
|
() => [
|
||||||
|
query.account,
|
||||||
|
query.token,
|
||||||
|
query.remark,
|
||||||
|
query.status,
|
||||||
|
query.platform,
|
||||||
|
query.usable,
|
||||||
|
activeTypeTab.value,
|
||||||
|
],
|
||||||
() => {
|
() => {
|
||||||
if (skipWatchFetchDuringUnusedJump.value) return;
|
if (skipWatchFetchDuringUnusedJump.value) return;
|
||||||
pagination.page = 1;
|
pagination.page = 1;
|
||||||
@ -183,7 +200,7 @@ function buildCopyTextByRow(row) {
|
|||||||
if (row?.account) parts.push(row.account);
|
if (row?.account) parts.push(row.account);
|
||||||
if (row?.password) parts.push(row.password);
|
if (row?.password) parts.push(row.password);
|
||||||
if (row?.token) parts.push(row.token);
|
if (row?.token) parts.push(row.token);
|
||||||
return parts.join('\n');
|
return parts.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowToText(row) {
|
function rowToText(row) {
|
||||||
@ -191,20 +208,20 @@ function rowToText(row) {
|
|||||||
if (row?.account) parts.push(row.account);
|
if (row?.account) parts.push(row.account);
|
||||||
if (row?.password) parts.push(row.password);
|
if (row?.password) parts.push(row.password);
|
||||||
if (row?.token) parts.push(row.token);
|
if (row?.token) parts.push(row.token);
|
||||||
return parts.join(' / ');
|
return parts.join(" / ");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyToClipboard(text) {
|
async function copyToClipboard(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
ElMessage.warning('无可复制内容');
|
ElMessage.warning("无可复制内容");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
ElMessage.success('已复制');
|
ElMessage.success("已复制");
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('复制失败,请检查浏览器权限');
|
ElMessage.error("复制失败,请检查浏览器权限");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -378,16 +395,20 @@ function extractStatusTagType(row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tooltipOpts = {
|
const tooltipOpts = {
|
||||||
popperClass: 'pool-tooltip',
|
popperClass: "pool-tooltip",
|
||||||
popperStyle: { maxWidth: '600px', wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
|
popperStyle: {
|
||||||
|
maxWidth: "600px",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const PLATFORM_MAP = {
|
const PLATFORM_MAP = {
|
||||||
local: { label: '本地', type: 'info' },
|
local: { label: "本地", type: "info" },
|
||||||
xianyu: { label: '闲鱼', type: 'warning' },
|
xianyu: { label: "闲鱼", type: "warning" },
|
||||||
pinduoduo: { label: '拼多多', type: 'danger' },
|
pinduoduo: { label: "拼多多", type: "danger" },
|
||||||
jingdong: { label: '京东', type: 'primary' },
|
jingdong: { label: "京东", type: "primary" },
|
||||||
douyin: { label: '抖音', type: 'success' },
|
douyin: { label: "抖音", type: "success" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function platformText(platform) {
|
function platformText(platform) {
|
||||||
@ -416,12 +437,17 @@ function isUsedTagType(isUsed) {
|
|||||||
function decodeJwtPayload(rawToken) {
|
function decodeJwtPayload(rawToken) {
|
||||||
const token = String(rawToken || "").trim();
|
const token = String(rawToken || "").trim();
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
const pureToken = token.includes("::") ? token.split("::").pop().trim() : token;
|
const pureToken = token.includes("::")
|
||||||
|
? token.split("::").pop().trim()
|
||||||
|
: token;
|
||||||
const parts = pureToken.split(".");
|
const parts = pureToken.split(".");
|
||||||
if (parts.length < 2) return null;
|
if (parts.length < 2) return null;
|
||||||
try {
|
try {
|
||||||
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
||||||
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
|
const padded = base64.padEnd(
|
||||||
|
base64.length + ((4 - (base64.length % 4)) % 4),
|
||||||
|
"=",
|
||||||
|
);
|
||||||
const json = decodeURIComponent(
|
const json = decodeURIComponent(
|
||||||
atob(padded)
|
atob(padded)
|
||||||
.split("")
|
.split("")
|
||||||
@ -528,7 +554,8 @@ async function fetchList() {
|
|||||||
remark: query.remark || undefined,
|
remark: query.remark || undefined,
|
||||||
status: query.status || undefined,
|
status: query.status || undefined,
|
||||||
platform: query.platform || undefined,
|
platform: query.platform || undefined,
|
||||||
usable: query.usable === "1" || query.usable === "0" ? query.usable : undefined,
|
usable:
|
||||||
|
query.usable === "1" || query.usable === "0" ? query.usable : undefined,
|
||||||
type: activeTypeTab.value === "all" ? undefined : activeTypeTab.value,
|
type: activeTypeTab.value === "all" ? undefined : activeTypeTab.value,
|
||||||
});
|
});
|
||||||
if (res?.code !== 200) {
|
if (res?.code !== 200) {
|
||||||
@ -553,7 +580,8 @@ async function jumpToLastUnusedPage() {
|
|||||||
token: query.token || undefined,
|
token: query.token || undefined,
|
||||||
remark: query.remark || undefined,
|
remark: query.remark || undefined,
|
||||||
status: "unused",
|
status: "unused",
|
||||||
usable: query.usable === "1" || query.usable === "0" ? query.usable : undefined,
|
usable:
|
||||||
|
query.usable === "1" || query.usable === "0" ? query.usable : undefined,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
if (res?.code !== 200) {
|
if (res?.code !== 200) {
|
||||||
@ -581,29 +609,44 @@ function updateDeviceType() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateDeviceType();
|
updateDeviceType();
|
||||||
window.addEventListener('resize', updateDeviceType);
|
window.addEventListener("resize", updateDeviceType);
|
||||||
fetchList();
|
fetchList();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', updateDeviceType);
|
window.removeEventListener("resize", updateDeviceType);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 接口说明数据 ----
|
// ---- 接口说明数据 ----
|
||||||
const BASE_URL = "https://api.yunzer.cn";
|
const BASE_URL = "https://api.yunzer.cn";
|
||||||
|
|
||||||
const paramDocs = [
|
const paramDocs = [
|
||||||
{ name: 'type', required: true, desc: '来源平台,用于标记本次提取来自哪个渠道', values: 'xianyu / taobao / pinduoduo / jingdong / local' },
|
{
|
||||||
{ name: 'module', required: true, desc: '号池模块,指定从哪个产品的号池提取', values: 'cursor / windsurf / krio' },
|
name: "type",
|
||||||
{ name: 'data_type', required: false, desc: '账号类型,不传则提取任意类型', values: 'account / tk / account_tk' },
|
required: true,
|
||||||
|
desc: "来源平台,用于标记本次提取来自哪个渠道",
|
||||||
|
values: "xianyu / taobao / pinduoduo / jingdong / local",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "module",
|
||||||
|
required: true,
|
||||||
|
desc: "号池模块,指定从哪个产品的号池提取",
|
||||||
|
values: "cursor / windsurf / krio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "data_type",
|
||||||
|
required: false,
|
||||||
|
desc: "账号类型,不传则提取任意类型",
|
||||||
|
values: "account / tk / account_tk",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const platformDocs = [
|
const platformDocs = [
|
||||||
{ value: 'xianyu', label: '闲鱼', desc: '闲鱼平台发货调用' },
|
{ value: "xianyu", label: "闲鱼", desc: "闲鱼平台发货调用" },
|
||||||
{ value: 'pinduoduo', label: '拼多多', desc: '拼多多平台发货调用' },
|
{ value: "pinduoduo", label: "拼多多", desc: "拼多多平台发货调用" },
|
||||||
{ value: 'jingdong', label: '京东', desc: '京东平台发货调用' },
|
{ value: "jingdong", label: "京东", desc: "京东平台发货调用" },
|
||||||
{ value: 'douyin', label: '抖音', desc: '抖音平台发货调用' },
|
{ value: "douyin", label: "抖音", desc: "抖音平台发货调用" },
|
||||||
{ value: 'local', label: '本地', desc: '本地手动调用' },
|
{ value: "local", label: "本地", desc: "本地手动调用" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const moduleDocs = [
|
const moduleDocs = [
|
||||||
@ -657,26 +700,48 @@ function copyCardInfo(row) {
|
|||||||
if (row.account) parts.push(row.account);
|
if (row.account) parts.push(row.account);
|
||||||
if (row.password) parts.push(row.password);
|
if (row.password) parts.push(row.password);
|
||||||
if (row.token) parts.push(row.token);
|
if (row.token) parts.push(row.token);
|
||||||
if (!parts.length) { ElMessage.warning('无可复制内容'); return; }
|
if (!parts.length) {
|
||||||
navigator.clipboard.writeText(parts.join('\n')).then(() => {
|
ElMessage.warning("无可复制内容");
|
||||||
ElMessage.success('已复制');
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(parts.join("\n")).then(() => {
|
||||||
|
ElMessage.success("已复制");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const CURSOR_PRO_LIMIT_TEXT = 'Get Cursor Pro for more Agent usage, unlimited Tab, and more.';
|
const CURSOR_PRO_LIMIT_TEXT =
|
||||||
|
"Get Cursor Pro for more Agent usage, unlimited Tab, and more.";
|
||||||
|
|
||||||
function formatCursorProbeDialogText(d) {
|
function formatCursorProbeDialogText(d) {
|
||||||
const detail = String(d?.detail || '');
|
const detail = String(d?.detail || "").trim();
|
||||||
const rawPreview = String(d?.rawPreview || '');
|
const rawPreview = String(d?.rawPreview || "").trim();
|
||||||
if (detail.includes(CURSOR_PRO_LIMIT_TEXT) || rawPreview.includes(CURSOR_PRO_LIMIT_TEXT)) {
|
const serverOutput = detail || rawPreview;
|
||||||
return '该TOKEN已用完';
|
|
||||||
|
// 1. 优先以新版后端的 ok 字段(也就是底层探针的交叉判定结论)为核心准则
|
||||||
|
if (d && typeof d.ok === "boolean") {
|
||||||
|
if (d.ok) {
|
||||||
|
return serverOutput || "该TOKEN可用";
|
||||||
}
|
}
|
||||||
return '该TOKEN可用';
|
return `该TOKEN已用完 (${detail || rawPreview || "额度枯竭"})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 兼容旧数据的兜底检测
|
||||||
|
const CURSOR_PRO_LIMIT_TEXT =
|
||||||
|
"Get Cursor Pro for more Agent usage, unlimited Tab, and more.";
|
||||||
|
|
||||||
|
if (
|
||||||
|
detail.includes(CURSOR_PRO_LIMIT_TEXT) ||
|
||||||
|
rawPreview.includes(CURSOR_PRO_LIMIT_TEXT)
|
||||||
|
) {
|
||||||
|
return `该TOKEN已用完 (${detail || rawPreview})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverOutput || "该TOKEN可用";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleProbeToken(row) {
|
async function handleProbeToken(row) {
|
||||||
if (!row?.token) {
|
if (!row?.token) {
|
||||||
ElMessage.warning('该行无 Token');
|
ElMessage.warning("该行无 Token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
probeLoadingId.value = row.id;
|
probeLoadingId.value = row.id;
|
||||||
@ -686,17 +751,17 @@ async function handleProbeToken(row) {
|
|||||||
accessToken: row.token,
|
accessToken: row.token,
|
||||||
});
|
});
|
||||||
if (res?.code !== 200) {
|
if (res?.code !== 200) {
|
||||||
ElMessage.error(res?.msg || '探测失败');
|
ElMessage.error(res?.msg || "探测失败");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = res?.data || {};
|
const d = res?.data || {};
|
||||||
const text = formatCursorProbeDialogText(d);
|
const text = formatCursorProbeDialogText(d);
|
||||||
try {
|
try {
|
||||||
await ElMessageBox({
|
await ElMessageBox({
|
||||||
title: '检测结果',
|
title: "检测结果",
|
||||||
message: h('div', { class: 'cursor-probe-result' }, text),
|
message: h("div", { class: "cursor-probe-result" }, text),
|
||||||
confirmButtonText: '关闭',
|
confirmButtonText: "关闭",
|
||||||
customClass: 'cursor-probe-dialog',
|
customClass: "cursor-probe-dialog",
|
||||||
closeOnClickModal: true,
|
closeOnClickModal: true,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@ -704,7 +769,7 @@ async function handleProbeToken(row) {
|
|||||||
}
|
}
|
||||||
await fetchList();
|
await fetchList();
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('探测请求失败');
|
ElMessage.error("探测请求失败");
|
||||||
} finally {
|
} finally {
|
||||||
probeLoadingId.value = null;
|
probeLoadingId.value = null;
|
||||||
}
|
}
|
||||||
@ -721,8 +786,7 @@ async function handleBatchProbe() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const skipped = selectedRows.value.length - rows.length;
|
const skipped = selectedRows.value.length - rows.length;
|
||||||
const skipHint =
|
const skipHint = skipped > 0 ? `(已跳过 ${skipped} 条无 Token)` : "";
|
||||||
skipped > 0 ? `(已跳过 ${skipped} 条无 Token)` : "";
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`将对 ${rows.length} 条 Token进行检测,是否继续?${skipHint}`,
|
`将对 ${rows.length} 条 Token进行检测,是否继续?${skipHint}`,
|
||||||
@ -812,7 +876,6 @@ async function handleBatchProbe() {
|
|||||||
// closeOnClickModal: true,
|
// closeOnClickModal: true,
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -888,8 +951,12 @@ async function handleBatchProbe() {
|
|||||||
<el-button @click="resetQuery">重置</el-button>
|
<el-button @click="resetQuery">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<el-button type="primary" @click="openAddDialog('single')">添加账号</el-button>
|
<el-button type="primary" @click="openAddDialog('single')"
|
||||||
<el-button type="success" @click="openAddDialog('batch')">批量添加</el-button>
|
>添加账号</el-button
|
||||||
|
>
|
||||||
|
<el-button type="success" @click="openAddDialog('batch')"
|
||||||
|
>批量添加</el-button
|
||||||
|
>
|
||||||
<el-button @click="replenishVisible = true">补号</el-button>
|
<el-button @click="replenishVisible = true">补号</el-button>
|
||||||
<el-button @click="markExtractForSelected">批量提取</el-button>
|
<el-button @click="markExtractForSelected">批量提取</el-button>
|
||||||
<el-button plain @click="handleBatchProbe">批量检测</el-button>
|
<el-button plain @click="handleBatchProbe">批量检测</el-button>
|
||||||
@ -924,7 +991,13 @@ async function handleBatchProbe() {
|
|||||||
<el-tag>{{ typeText(row.type) }}</el-tag>
|
<el-tag>{{ typeText(row.type) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="account" label="账号" min-width="180" show-overflow-tooltip :tooltip-options="tooltipOpts" />
|
<el-table-column
|
||||||
|
prop="account"
|
||||||
|
label="账号"
|
||||||
|
min-width="180"
|
||||||
|
show-overflow-tooltip
|
||||||
|
:tooltip-options="tooltipOpts"
|
||||||
|
/>
|
||||||
<!-- <el-table-column prop="password" label="密码" min-width="160" show-overflow-tooltip :tooltip-options="tooltipOpts">
|
<!-- <el-table-column prop="password" label="密码" min-width="160" show-overflow-tooltip :tooltip-options="tooltipOpts">
|
||||||
<template #default="{ row }">{{ row.password || '-' }}</template>
|
<template #default="{ row }">{{ row.password || '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -945,13 +1018,20 @@ async function handleBatchProbe() {
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="accessToken失效时间" width="190" align="center">
|
<!-- <el-table-column
|
||||||
|
label="accessToken失效时间"
|
||||||
|
width="190"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="accessTokenExpireTagType(row.accessTokenExpireStatus)" size="small">
|
<el-tag
|
||||||
|
:type="accessTokenExpireTagType(row.accessTokenExpireStatus)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
{{ row.accessTokenExpireText || "-" }}
|
{{ row.accessTokenExpireText || "-" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column> -->
|
||||||
<el-table-column label="提取平台" width="120">
|
<el-table-column label="提取平台" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag
|
<el-tag
|
||||||
@ -965,8 +1045,19 @@ async function handleBatchProbe() {
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="extractedAt" label="提取时间" width="180" />
|
<el-table-column prop="extractedAt" label="提取时间" width="180" />
|
||||||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip :tooltip-options="tooltipOpts" />
|
<el-table-column
|
||||||
<el-table-column label="操作" width="300" fixed="right" align="center">
|
prop="remark"
|
||||||
|
label="备注"
|
||||||
|
min-width="140"
|
||||||
|
show-overflow-tooltip
|
||||||
|
:tooltip-options="tooltipOpts"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
label="操作"
|
||||||
|
width="300"
|
||||||
|
fixed="right"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="row.token"
|
v-if="row.token"
|
||||||
@ -1003,7 +1094,9 @@ async function handleBatchProbe() {
|
|||||||
v-model:current-page="pagination.page"
|
v-model:current-page="pagination.page"
|
||||||
v-model:page-size="pagination.pageSize"
|
v-model:page-size="pagination.pageSize"
|
||||||
background
|
background
|
||||||
:layout="isMobile ? 'prev, pager, next' : 'total, prev, pager, next, jumper'"
|
:layout="
|
||||||
|
isMobile ? 'prev, pager, next' : 'total, prev, pager, next, jumper'
|
||||||
|
"
|
||||||
:page-sizes="[20, 50, 100]"
|
:page-sizes="[20, 50, 100]"
|
||||||
:total="total"
|
:total="total"
|
||||||
/>
|
/>
|
||||||
@ -1055,7 +1148,9 @@ async function handleBatchProbe() {
|
|||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<el-alert type="info" :closable="false" style="margin-bottom: 12px">
|
<el-alert type="info" :closable="false" style="margin-bottom: 12px">
|
||||||
将对已选的 <strong>{{ selectedRows.length }}</strong> 条记录执行提取并标记为已提取。
|
将对已选的
|
||||||
|
<strong>{{ selectedRows.length }}</strong>
|
||||||
|
条记录执行提取并标记为已提取。
|
||||||
</el-alert>
|
</el-alert>
|
||||||
<el-form label-width="84px">
|
<el-form label-width="84px">
|
||||||
<el-form-item label="提取平台">
|
<el-form-item label="提取平台">
|
||||||
@ -1079,7 +1174,11 @@ async function handleBatchProbe() {
|
|||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="batchExtractVisible = false">取消</el-button>
|
<el-button @click="batchExtractVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="loading" @click="handleBatchExtract">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleBatchExtract"
|
||||||
|
>
|
||||||
确认提取
|
确认提取
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
@ -1299,7 +1398,6 @@ async function handleBatchProbe() {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 8px 12px 12px;
|
padding: 8px 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 接口说明抽屉 */
|
/* 接口说明抽屉 */
|
||||||
@ -1421,10 +1519,13 @@ async function handleBatchProbe() {
|
|||||||
}
|
}
|
||||||
.cursor-probe-dialog .cursor-probe-result {
|
.cursor-probe-dialog .cursor-probe-result {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
max-height: 420px;
|
||||||
font-size: 16px;
|
overflow: auto;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.cursor-probe-dialog .cursor-expire-result {
|
.cursor-probe-dialog .cursor-expire-result {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user