更新检测流程
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) {
|
||||
for {
|
||||
c.extractWithProbe("cursor", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolCursor
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||
Filter("is_extracted", 0).
|
||||
@ -100,6 +100,55 @@ func (c *ApiGetCardController) extractCursor(platform, dataType string, now time
|
||||
qs = qs.Filter("data_type", dataType)
|
||||
}
|
||||
if err := qs.OrderBy("id").One(&row); err != nil {
|
||||
return 0, nil, nil, "", "", nil, err
|
||||
}
|
||||
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, row.IsUsed, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractWindsurf(platform, dataType string, now time.Time) {
|
||||
c.extractWithProbe("windsurf", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolWindsurf
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
||||
Filter("is_extracted", 0).
|
||||
Filter("delete_time__isnull", true)
|
||||
if dataType != "" {
|
||||
qs = qs.Filter("data_type", dataType)
|
||||
}
|
||||
if err := qs.OrderBy("id").One(&row); err != nil {
|
||||
return 0, nil, nil, "", "", nil, err
|
||||
}
|
||||
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractKrio(platform, dataType string, now time.Time) {
|
||||
c.extractWithProbe("krio", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolKiro
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
|
||||
Filter("is_extracted", 0).
|
||||
Filter("delete_time__isnull", true)
|
||||
if dataType != "" {
|
||||
qs = qs.Filter("data_type", dataType)
|
||||
}
|
||||
if err := qs.OrderBy("id").One(&row); err != nil {
|
||||
return 0, nil, nil, "", "", nil, err
|
||||
}
|
||||
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
type poolRowFetcher func() (id uint64, account, password *string, token, rowDataType string, isUsed *int8, err error)
|
||||
|
||||
// extractWithProbe 按 id 顺序提取并探测 Token 可用性;不可用则标记已提取并继续下一条。
|
||||
func (c *ApiGetCardController) extractWithProbe(
|
||||
module, platform, dataType string,
|
||||
now time.Time,
|
||||
fetch poolRowFetcher,
|
||||
) {
|
||||
for {
|
||||
id, account, password, token, rowDataType, isUsed, err := fetch()
|
||||
if err != nil {
|
||||
if err == orm.ErrNoRows {
|
||||
c.cardErr(404, 404, "暂无可用卡密")
|
||||
} else {
|
||||
@ -108,85 +157,40 @@ func (c *ApiGetCardController) extractCursor(platform, dataType string, now time
|
||||
return
|
||||
}
|
||||
|
||||
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||
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{}{
|
||||
"is_extracted": 1,
|
||||
"extracted_time": now,
|
||||
"extracted_platform": platform,
|
||||
"update_time": now,
|
||||
})
|
||||
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
|
||||
// 已有探测结论:可用则直接返回,不可用则继续下一条。
|
||||
if known, available := poolIsUsedAvailable(isUsed); known {
|
||||
if available {
|
||||
c.cardOK(buildCardResult(account, password, token, rowDataType))
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractWindsurf(platform, dataType string, now time.Time) {
|
||||
var row models.PlatformAccountPoolWindsurf
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
||||
Filter("is_extracted", 0).
|
||||
Filter("delete_time__isnull", true)
|
||||
if dataType != "" {
|
||||
qs = qs.Filter("data_type", dataType)
|
||||
}
|
||||
if err := qs.OrderBy("id").One(&row); err != nil {
|
||||
if err == orm.ErrNoRows {
|
||||
c.cardErr(404, 404, "暂无可用卡密")
|
||||
} else {
|
||||
c.cardErr(500, 500, "查询失败")
|
||||
if !poolProbeToken(module, rowDataType, token, id) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
||||
Filter("id", row.ID).
|
||||
Update(map[string]interface{}{
|
||||
"is_extracted": 1,
|
||||
"extracted_time": now,
|
||||
"extracted_platform": platform,
|
||||
})
|
||||
if err != nil {
|
||||
c.cardErr(500, 500, "提取失败")
|
||||
return
|
||||
}
|
||||
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType))
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractKrio(platform, dataType string, now time.Time) {
|
||||
var row models.PlatformAccountPoolKiro
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
|
||||
Filter("is_extracted", 0).
|
||||
Filter("delete_time__isnull", true)
|
||||
if dataType != "" {
|
||||
qs = qs.Filter("data_type", dataType)
|
||||
}
|
||||
if err := qs.OrderBy("id").One(&row); err != nil {
|
||||
if err == orm.ErrNoRows {
|
||||
c.cardErr(404, 404, "暂无可用卡密")
|
||||
} else {
|
||||
c.cardErr(500, 500, "查询失败")
|
||||
}
|
||||
c.cardOK(buildCardResult(account, password, token, rowDataType))
|
||||
return
|
||||
}
|
||||
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
|
||||
Filter("id", row.ID).
|
||||
Update(map[string]interface{}{
|
||||
"is_extracted": 1,
|
||||
"extracted_time": now,
|
||||
"extracted_platform": platform,
|
||||
})
|
||||
if err != nil {
|
||||
c.cardErr(500, 500, "提取失败")
|
||||
return
|
||||
}
|
||||
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType))
|
||||
}
|
||||
|
||||
// buildCardResult 根据账号类型返回格式化字符串
|
||||
|
||||
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 {
|
||||
case "cursor":
|
||||
var row models.PlatformAccountPoolCursor
|
||||
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||
Filter("is_extracted", 0).Filter("data_type", payload.Type).
|
||||
OrderBy("id").One(&row); err != nil {
|
||||
poolJSONErr(c, 404, 404, "暂无可用账号")
|
||||
return
|
||||
checkedCount := 0
|
||||
unavailableCount := 0
|
||||
|
||||
for {
|
||||
var row models.PlatformAccountPoolCursor
|
||||
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||
Filter("is_extracted", 0).Filter("data_type", payload.Type).
|
||||
OrderBy("id").One(&row); err != nil {
|
||||
msg := "暂无可用账号"
|
||||
if checkedCount > 0 {
|
||||
msg = fmt.Sprintf("已检测%d个账号,其中%d个不可用,暂无可用账号", checkedCount, unavailableCount)
|
||||
}
|
||||
poolJSONErr(c, 404, 404, msg)
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
poolJSONErr(c, 500, 500, "补号失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
row.IsExtracted = 2
|
||||
isUsed := int8(1)
|
||||
row.IsUsed = &isUsed
|
||||
row.ExtractedTime = &now
|
||||
row.ExtractedPlatform = &platform
|
||||
row.Remark = remark
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "补号成功",
|
||||
"data": row,
|
||||
"probe": map[string]interface{}{
|
||||
"checkedCount": checkedCount,
|
||||
"unavailableCount": unavailableCount,
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
if _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", row.ID).Update(map[string]interface{}{
|
||||
"is_extracted": int8(2), "extracted_time": now, "extracted_platform": platform, "remark": remark,
|
||||
}); err != nil {
|
||||
poolJSONErr(c, 500, 500, "补号失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
row.IsExtracted = 2
|
||||
row.ExtractedTime = &now
|
||||
row.ExtractedPlatform = &platform
|
||||
row.Remark = remark
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "补号成功", "data": row}
|
||||
case "windsurf":
|
||||
var row models.PlatformAccountPoolWindsurf
|
||||
if err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
||||
@ -973,12 +1019,12 @@ func probePoolToken(c *beego.Controller, module string) {
|
||||
if r.StreamNote != "" {
|
||||
data["streamNote"] = r.StreamNote
|
||||
}
|
||||
// Cursor 探测状态只按底层探针结论 r.OK 保存。
|
||||
// 注意:客户端版本过旧只是 warning,Token 仍可用时 r.OK=true,不能因此写成已用完。
|
||||
if module == "cursor" && payload.ID > 0 && r.HTTPStatus == http.StatusOK {
|
||||
var isUsed int8
|
||||
isUsed := int8(0)
|
||||
if r.OK {
|
||||
isUsed = 1
|
||||
} else {
|
||||
isUsed = 0
|
||||
}
|
||||
if _, uerr := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", payload.ID).Update(orm.Params{
|
||||
"is_used": isUsed,
|
||||
@ -995,6 +1041,62 @@ func probePoolToken(c *beego.Controller, module string) {
|
||||
_ = 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) Add() { addPoolRow(&c.Controller, "cursor") }
|
||||
func (c *PlatformAccountPoolCursorController) BatchAdd() { batchAddPoolRows(&c.Controller, "cursor") }
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -45,12 +44,6 @@ func requirePlatform(c *beego.Controller) (*jwtutil.Claims, error) {
|
||||
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=
|
||||
@ -150,12 +143,6 @@ func (c *PlatformDomainPoolController) GetEnabledDomains() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type domainPoolPayload struct {
|
||||
ID uint64 `json:"id"`
|
||||
MainDomain string `json:"main_domain"`
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
// Create POST /platform/domain/pool/create
|
||||
func (c *PlatformDomainPoolController) Create() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
@ -397,8 +384,6 @@ func (c *PlatformTenantDomainController) MyDomains() {
|
||||
_ = 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}
|
||||
func (c *PlatformTenantDomainController) Apply() {
|
||||
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(PlatformComplaint),
|
||||
new(SystemSoftwareUpgrade),
|
||||
new(CmsArticle),
|
||||
new(CmsArticleCategory),
|
||||
new(PlatformAccountPoolKiro),
|
||||
new(PlatformAccountPoolWindsurf),
|
||||
new(PlatformAccountPoolCursor),
|
||||
|
||||
BIN
go/output.log
Normal file
BIN
go/output.log
Normal file
Binary file not shown.
@ -10,6 +10,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@ -22,7 +23,7 @@ import (
|
||||
const (
|
||||
cursorBackendURL = "https://api2.cursor.sh"
|
||||
cursorAgentPath = "/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
|
||||
cursorClientVersion = "2.6.22"
|
||||
cursorClientVersion = "3.6.31"
|
||||
cursorHiMaxRead = 512 * 1024
|
||||
// probeHiText 发往官方 Agent 的探测内容(与前端展示 probeMessage 一致)
|
||||
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"
|
||||
|
||||
// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示(ASCII 区不区分大小写 + UTF-8 短语)
|
||||
func classifyCursorRawStream(raw []byte) (blocked bool, reason string) {
|
||||
// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示
|
||||
// 返回 (isQuotaExhausted, message)
|
||||
// isQuotaExhausted: true 表示额度用完/Token不可用,false 表示 Token 可用(可能有警告信息)
|
||||
func classifyCursorRawStream(raw []byte) (isQuotaExhausted bool, message string) {
|
||||
if len(raw) == 0 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// 额度用尽只按明确完整提示判定,避免“可用但带推广/提示文案”的 Token 被误标为已用完。
|
||||
// 用户自定义的二进制特征仍保留给部署方精确配置。
|
||||
for _, sig := range cursorQuotaExhaustedSigsFromEnv() {
|
||||
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) {
|
||||
return true, "流中匹配:" + string(cursorQuotaTipSig)
|
||||
}
|
||||
// 社区脚本:仅到「…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"
|
||||
return true, "该TOKEN已用完(Get Cursor Pro for more Agent usage, unlimited Tab, and more.)"
|
||||
}
|
||||
|
||||
flat := strings.ToLower(strings.ToValidUTF8(string(raw), "\uFFFD"))
|
||||
flat = strings.ReplaceAll(flat, "\u2019", "'") // 右单引号
|
||||
flat = strings.ReplaceAll(flat, "\u2019", "'")
|
||||
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, ""
|
||||
}
|
||||
|
||||
@ -400,41 +389,29 @@ func decodeConnectFramedBody(raw []byte) ([]byte, string, bool) {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
note := fmt.Sprintf("响应体已按 Connect 分帧解析(%d 帧", frameCount)
|
||||
if compressedFrames > 0 {
|
||||
note += fmt.Sprintf(",其中 %d 帧已做 gzip 解压", compressedFrames)
|
||||
}
|
||||
note += ")后分析"
|
||||
return out.Bytes(), note, true
|
||||
return out.Bytes(), "", true
|
||||
}
|
||||
|
||||
func decodeCursorResponseBody(raw []byte, contentEncoding string) ([]byte, string) {
|
||||
if decoded, note, ok := decodeConnectFramedBody(raw); ok {
|
||||
return decoded, note
|
||||
if decoded, _, ok := decodeConnectFramedBody(raw); ok {
|
||||
return decoded, ""
|
||||
}
|
||||
|
||||
enc := strings.ToLower(strings.TrimSpace(contentEncoding))
|
||||
if strings.Contains(enc, "gzip") || looksLikeGzip(raw) {
|
||||
decoded, err := gunzipBytes(raw)
|
||||
if err != nil {
|
||||
if strings.Contains(enc, "gzip") {
|
||||
return raw, "响应头声明 gzip,但解压失败,已回退为原始字节预览"
|
||||
}
|
||||
return raw, "检测到 gzip 魔数,但解压失败,已回退为原始字节预览"
|
||||
return raw, ""
|
||||
}
|
||||
if strings.Contains(enc, "gzip") {
|
||||
return decoded, "响应体已按 gzip 解压后分析"
|
||||
}
|
||||
return decoded, "响应体虽未显式声明 Content-Encoding,但按 gzip 魔数解压后分析"
|
||||
return decoded, ""
|
||||
}
|
||||
if enc != "" {
|
||||
return raw, "响应头 Content-Encoding=" + enc + ",当前未额外解码,按原始字节分析"
|
||||
}
|
||||
return raw, "响应体未压缩或未声明压缩,且未识别为 Connect 分帧,按原始字节分析"
|
||||
return raw, ""
|
||||
}
|
||||
|
||||
// cursorStreamProtocol 与官方客户端一致:Connect-RPC + protobuf 体,HTTP/2 流式
|
||||
const cursorStreamProtocol = "Connect-Protocol-Version:1 + application/connect+proto,HTTP/2 二进制流(gRPC 兼容形态,非 JSON REST)"
|
||||
// cursorStreamProtocol 与官方客户端一致:Connect-RPC + protobuf 体,HTTP/2 流式。
|
||||
// 当前探测接口使用新版 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」结论一致)
|
||||
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 {
|
||||
if strings.Contains(authToken, "::") {
|
||||
if i := strings.LastIndex(authToken, "::"); i >= 0 {
|
||||
@ -504,7 +664,7 @@ func probeCursorHiAgent(authToken string) Result {
|
||||
fullURL := cursorBackendURL + cursorAgentPath
|
||||
req, err := http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
r := cursorProbeResult(false, err.Error(), 0, body, nil, nil)
|
||||
r := cursorProbeResult(false, "请求失败: "+err.Error(), 0, body, nil, nil)
|
||||
return r
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
@ -532,29 +692,36 @@ func probeCursorHiAgent(authToken string) Result {
|
||||
|
||||
resp, err := cursorProbeHTTPClient.Do(req)
|
||||
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()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, cursorHiMaxRead))
|
||||
decoded, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
blocked, reason := classifyCursorRawStream(decoded)
|
||||
if blocked {
|
||||
return cursorProbeResult(false, reason+";"+decodeNote, resp.StatusCode, body, raw, decoded)
|
||||
decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
isQuotaExhausted, msg := classifyCursorRawStream(decoded)
|
||||
if isQuotaExhausted {
|
||||
return cursorProbeResult(false, msg, resp.StatusCode, body, raw, decoded)
|
||||
}
|
||||
detail := fmt.Sprintf("HTTP %d(非 200);%s;说明与协议边界见 streamNote", resp.StatusCode, decodeNote)
|
||||
return cursorProbeResult(false, detail, resp.StatusCode, body, raw, decoded)
|
||||
// 非 200 状态码且不是额度问题
|
||||
return cursorProbeResult(false, fmt.Sprintf("HTTP %d - Token不可用", resp.StatusCode), resp.StatusCode, body, raw, decoded)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, io.LimitReader(resp.Body, cursorHiMaxRead))
|
||||
raw := buf.Bytes()
|
||||
decoded, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
blocked, reason := classifyCursorRawStream(decoded)
|
||||
if blocked {
|
||||
return cursorProbeResult(false, reason+";"+decodeNote, resp.StatusCode, body, raw, decoded)
|
||||
decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
isQuotaExhausted, msg := classifyCursorRawStream(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,9 +26,8 @@ type Result struct {
|
||||
BytesRead int `json:"bytesRead,omitempty"`
|
||||
RawPreview string `json:"rawPreview,omitempty"`
|
||||
RequestBodyPrefixHex string `json:"requestBodyPrefixHex,omitempty"`
|
||||
// StreamProtocol / StreamNote 仅 Cursor Agent 探测填充,说明二进制流与结论边界
|
||||
StreamProtocol string `json:"streamProtocol,omitempty"`
|
||||
StreamNote string `json:"streamNote,omitempty"`
|
||||
StreamProtocol string `json:"streamProtocol,omitempty"`
|
||||
StreamNote string `json:"streamNote,omitempty"`
|
||||
}
|
||||
|
||||
// ProbeOfficial 按号池模块探测 Token(cursor / windsurf / krio)
|
||||
@ -39,7 +38,8 @@ func ProbeOfficial(module, rawToken string) Result {
|
||||
}
|
||||
switch module {
|
||||
case "cursor":
|
||||
return probeCursor(tok)
|
||||
// 直接使用 cursor_hi.go 中已有的完整探测函数
|
||||
return probeCursorHiAgent(tok)
|
||||
case "windsurf":
|
||||
return probeWindsurf(tok)
|
||||
case "krio":
|
||||
@ -57,19 +57,21 @@ func normalizeBearerToken(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// probeCursor Cursor Token 探测(直接使用 cursor_hi.go 的实现)
|
||||
func probeCursor(token string) Result {
|
||||
return probeCursorHiAgent(token)
|
||||
}
|
||||
|
||||
// probeWindsurf WindSurf 探测
|
||||
func probeWindsurf(apiKey string) Result {
|
||||
payload := map[string]interface{}{
|
||||
"metadata": map[string]string{
|
||||
"apiKey": apiKey,
|
||||
"ideName": "windsurf",
|
||||
"ideVersion": "0.0.0",
|
||||
"extensionName": "windsurf",
|
||||
"extensionVersion": "0.0.0",
|
||||
"locale": "zh",
|
||||
"apiKey": apiKey,
|
||||
"ideName": "windsurf",
|
||||
"ideVersion": "0.0.0",
|
||||
"extensionName": "windsurf",
|
||||
"extensionVersion": "0.0.0",
|
||||
"locale": "zh",
|
||||
},
|
||||
}
|
||||
raw, err := json.Marshal(payload)
|
||||
@ -117,6 +119,7 @@ func probeWindsurf(apiKey string) Result {
|
||||
}
|
||||
}
|
||||
|
||||
// probeKiro Kiro 探测
|
||||
func probeKiro(accessToken string) Result {
|
||||
arn := findProfileArnInJWT(accessToken)
|
||||
if arn == "" {
|
||||
@ -160,6 +163,7 @@ func probeKiro(accessToken string) Result {
|
||||
}
|
||||
}
|
||||
|
||||
// decodeJWTPayloadMap 解析 JWT payload
|
||||
func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
|
||||
tok := normalizeBearerToken(strings.TrimSpace(raw))
|
||||
parts := strings.Split(tok, ".")
|
||||
@ -177,6 +181,7 @@ func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// findProfileArnInJWT 从 JWT 中查找 profileArn
|
||||
func findProfileArnInJWT(raw string) string {
|
||||
m, err := decodeJWTPayloadMap(raw)
|
||||
if err != nil {
|
||||
@ -185,6 +190,7 @@ func findProfileArnInJWT(raw string) string {
|
||||
return findProfileArnValue(m)
|
||||
}
|
||||
|
||||
// findProfileArnValue 递归查找 profileArn
|
||||
func findProfileArnValue(v interface{}) string {
|
||||
switch x := v.(type) {
|
||||
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>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
<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 Edit from "./components/edit.vue";
|
||||
import DetailDialog from "./components/detail.vue";
|
||||
@ -88,7 +97,15 @@ const typeTabs = computed(() => {
|
||||
});
|
||||
|
||||
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;
|
||||
pagination.page = 1;
|
||||
@ -183,7 +200,7 @@ function buildCopyTextByRow(row) {
|
||||
if (row?.account) parts.push(row.account);
|
||||
if (row?.password) parts.push(row.password);
|
||||
if (row?.token) parts.push(row.token);
|
||||
return parts.join('\n');
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
function rowToText(row) {
|
||||
@ -191,20 +208,20 @@ function rowToText(row) {
|
||||
if (row?.account) parts.push(row.account);
|
||||
if (row?.password) parts.push(row.password);
|
||||
if (row?.token) parts.push(row.token);
|
||||
return parts.join(' / ');
|
||||
return parts.join(" / ");
|
||||
}
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
if (!text) {
|
||||
ElMessage.warning('无可复制内容');
|
||||
ElMessage.warning("无可复制内容");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
ElMessage.success('已复制');
|
||||
ElMessage.success("已复制");
|
||||
return true;
|
||||
} catch (e) {
|
||||
ElMessage.error('复制失败,请检查浏览器权限');
|
||||
ElMessage.error("复制失败,请检查浏览器权限");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -378,16 +395,20 @@ function extractStatusTagType(row) {
|
||||
}
|
||||
|
||||
const tooltipOpts = {
|
||||
popperClass: 'pool-tooltip',
|
||||
popperStyle: { maxWidth: '600px', wordBreak: 'break-all', whiteSpace: 'pre-wrap' },
|
||||
popperClass: "pool-tooltip",
|
||||
popperStyle: {
|
||||
maxWidth: "600px",
|
||||
wordBreak: "break-all",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
};
|
||||
|
||||
const PLATFORM_MAP = {
|
||||
local: { label: '本地', type: 'info' },
|
||||
xianyu: { label: '闲鱼', type: 'warning' },
|
||||
pinduoduo: { label: '拼多多', type: 'danger' },
|
||||
jingdong: { label: '京东', type: 'primary' },
|
||||
douyin: { label: '抖音', type: 'success' },
|
||||
local: { label: "本地", type: "info" },
|
||||
xianyu: { label: "闲鱼", type: "warning" },
|
||||
pinduoduo: { label: "拼多多", type: "danger" },
|
||||
jingdong: { label: "京东", type: "primary" },
|
||||
douyin: { label: "抖音", type: "success" },
|
||||
};
|
||||
|
||||
function platformText(platform) {
|
||||
@ -416,12 +437,17 @@ function isUsedTagType(isUsed) {
|
||||
function decodeJwtPayload(rawToken) {
|
||||
const token = String(rawToken || "").trim();
|
||||
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(".");
|
||||
if (parts.length < 2) return null;
|
||||
try {
|
||||
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(
|
||||
atob(padded)
|
||||
.split("")
|
||||
@ -528,7 +554,8 @@ async function fetchList() {
|
||||
remark: query.remark || undefined,
|
||||
status: query.status || 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,
|
||||
});
|
||||
if (res?.code !== 200) {
|
||||
@ -553,7 +580,8 @@ async function jumpToLastUnusedPage() {
|
||||
token: query.token || undefined,
|
||||
remark: query.remark || undefined,
|
||||
status: "unused",
|
||||
usable: query.usable === "1" || query.usable === "0" ? query.usable : undefined,
|
||||
usable:
|
||||
query.usable === "1" || query.usable === "0" ? query.usable : undefined,
|
||||
type,
|
||||
});
|
||||
if (res?.code !== 200) {
|
||||
@ -581,29 +609,44 @@ function updateDeviceType() {
|
||||
|
||||
onMounted(() => {
|
||||
updateDeviceType();
|
||||
window.addEventListener('resize', updateDeviceType);
|
||||
window.addEventListener("resize", updateDeviceType);
|
||||
fetchList();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateDeviceType);
|
||||
window.removeEventListener("resize", updateDeviceType);
|
||||
});
|
||||
|
||||
// ---- 接口说明数据 ----
|
||||
const BASE_URL = "https://api.yunzer.cn";
|
||||
|
||||
const paramDocs = [
|
||||
{ name: 'type', 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' },
|
||||
{
|
||||
name: "type",
|
||||
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 = [
|
||||
{ value: 'xianyu', label: '闲鱼', desc: '闲鱼平台发货调用' },
|
||||
{ value: 'pinduoduo', label: '拼多多', desc: '拼多多平台发货调用' },
|
||||
{ value: 'jingdong', label: '京东', desc: '京东平台发货调用' },
|
||||
{ value: 'douyin', label: '抖音', desc: '抖音平台发货调用' },
|
||||
{ value: 'local', label: '本地', desc: '本地手动调用' },
|
||||
{ value: "xianyu", label: "闲鱼", desc: "闲鱼平台发货调用" },
|
||||
{ value: "pinduoduo", label: "拼多多", desc: "拼多多平台发货调用" },
|
||||
{ value: "jingdong", label: "京东", desc: "京东平台发货调用" },
|
||||
{ value: "douyin", label: "抖音", desc: "抖音平台发货调用" },
|
||||
{ value: "local", label: "本地", desc: "本地手动调用" },
|
||||
];
|
||||
|
||||
const moduleDocs = [
|
||||
@ -657,26 +700,48 @@ function copyCardInfo(row) {
|
||||
if (row.account) parts.push(row.account);
|
||||
if (row.password) parts.push(row.password);
|
||||
if (row.token) parts.push(row.token);
|
||||
if (!parts.length) { ElMessage.warning('无可复制内容'); return; }
|
||||
navigator.clipboard.writeText(parts.join('\n')).then(() => {
|
||||
ElMessage.success('已复制');
|
||||
if (!parts.length) {
|
||||
ElMessage.warning("无可复制内容");
|
||||
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) {
|
||||
const detail = String(d?.detail || '');
|
||||
const rawPreview = String(d?.rawPreview || '');
|
||||
if (detail.includes(CURSOR_PRO_LIMIT_TEXT) || rawPreview.includes(CURSOR_PRO_LIMIT_TEXT)) {
|
||||
return '该TOKEN已用完';
|
||||
const detail = String(d?.detail || "").trim();
|
||||
const rawPreview = String(d?.rawPreview || "").trim();
|
||||
const serverOutput = detail || rawPreview;
|
||||
|
||||
// 1. 优先以新版后端的 ok 字段(也就是底层探针的交叉判定结论)为核心准则
|
||||
if (d && typeof d.ok === "boolean") {
|
||||
if (d.ok) {
|
||||
return serverOutput || "该TOKEN可用";
|
||||
}
|
||||
return `该TOKEN已用完 (${detail || rawPreview || "额度枯竭"})`;
|
||||
}
|
||||
return '该TOKEN可用';
|
||||
|
||||
// 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) {
|
||||
if (!row?.token) {
|
||||
ElMessage.warning('该行无 Token');
|
||||
ElMessage.warning("该行无 Token");
|
||||
return;
|
||||
}
|
||||
probeLoadingId.value = row.id;
|
||||
@ -686,17 +751,17 @@ async function handleProbeToken(row) {
|
||||
accessToken: row.token,
|
||||
});
|
||||
if (res?.code !== 200) {
|
||||
ElMessage.error(res?.msg || '探测失败');
|
||||
ElMessage.error(res?.msg || "探测失败");
|
||||
return;
|
||||
}
|
||||
const d = res?.data || {};
|
||||
const text = formatCursorProbeDialogText(d);
|
||||
try {
|
||||
await ElMessageBox({
|
||||
title: '检测结果',
|
||||
message: h('div', { class: 'cursor-probe-result' }, text),
|
||||
confirmButtonText: '关闭',
|
||||
customClass: 'cursor-probe-dialog',
|
||||
title: "检测结果",
|
||||
message: h("div", { class: "cursor-probe-result" }, text),
|
||||
confirmButtonText: "关闭",
|
||||
customClass: "cursor-probe-dialog",
|
||||
closeOnClickModal: true,
|
||||
});
|
||||
} catch {
|
||||
@ -704,7 +769,7 @@ async function handleProbeToken(row) {
|
||||
}
|
||||
await fetchList();
|
||||
} catch {
|
||||
ElMessage.error('探测请求失败');
|
||||
ElMessage.error("探测请求失败");
|
||||
} finally {
|
||||
probeLoadingId.value = null;
|
||||
}
|
||||
@ -721,8 +786,7 @@ async function handleBatchProbe() {
|
||||
return;
|
||||
}
|
||||
const skipped = selectedRows.value.length - rows.length;
|
||||
const skipHint =
|
||||
skipped > 0 ? `(已跳过 ${skipped} 条无 Token)` : "";
|
||||
const skipHint = skipped > 0 ? `(已跳过 ${skipped} 条无 Token)` : "";
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将对 ${rows.length} 条 Token进行检测,是否继续?${skipHint}`,
|
||||
@ -812,7 +876,6 @@ async function handleBatchProbe() {
|
||||
// closeOnClickModal: true,
|
||||
// });
|
||||
// }
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -888,8 +951,12 @@ async function handleBatchProbe() {
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" @click="openAddDialog('single')">添加账号</el-button>
|
||||
<el-button type="success" @click="openAddDialog('batch')">批量添加</el-button>
|
||||
<el-button type="primary" @click="openAddDialog('single')"
|
||||
>添加账号</el-button
|
||||
>
|
||||
<el-button type="success" @click="openAddDialog('batch')"
|
||||
>批量添加</el-button
|
||||
>
|
||||
<el-button @click="replenishVisible = true">补号</el-button>
|
||||
<el-button @click="markExtractForSelected">批量提取</el-button>
|
||||
<el-button plain @click="handleBatchProbe">批量检测</el-button>
|
||||
@ -917,84 +984,108 @@ async function handleBatchProbe() {
|
||||
:loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="52" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="账号类型" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ typeText(row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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 type="selection" width="52" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="账号类型" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ typeText(row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<template #default="{ row }">{{ row.password || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Token" min-width="200" show-overflow-tooltip :tooltip-options="tooltipOpts">
|
||||
<template #default="{ row }">{{ row.token || '-' }}</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="提取状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="extractStatusTagType(row)">
|
||||
{{ extractStatusLabel(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="探测可用" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="isUsedTagType(row.isUsed)" size="small">
|
||||
{{ isUsedLabel(row.isUsed) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="accessToken失效时间" width="190" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="accessTokenExpireTagType(row.accessTokenExpireStatus)" size="small">
|
||||
{{ row.accessTokenExpireText || "-" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提取平台" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.extractedPlatform"
|
||||
:type="platformTagType(row.extractedPlatform)"
|
||||
size="small"
|
||||
>
|
||||
{{ platformText(row.extractedPlatform) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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 label="操作" width="300" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.token"
|
||||
link
|
||||
type="info"
|
||||
:loading="probeLoadingId === row.id"
|
||||
@click="handleProbeToken(row)"
|
||||
>检测</el-button
|
||||
>
|
||||
<el-button link type="primary" @click="openDetail(row)"
|
||||
>详情</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="!row.extractedAt && !row.extracted"
|
||||
link
|
||||
type="warning"
|
||||
@click="openExtractByRow(row)"
|
||||
>提取</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="row.extracted"
|
||||
link
|
||||
type="success"
|
||||
@click="copyCardInfo(row)"
|
||||
>复制</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提取状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="extractStatusTagType(row)">
|
||||
{{ extractStatusLabel(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="探测可用" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="isUsedTagType(row.isUsed)" size="small">
|
||||
{{ isUsedLabel(row.isUsed) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column
|
||||
label="accessToken失效时间"
|
||||
width="190"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="accessTokenExpireTagType(row.accessTokenExpireStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ row.accessTokenExpireText || "-" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="提取平台" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.extractedPlatform"
|
||||
:type="platformTagType(row.extractedPlatform)"
|
||||
size="small"
|
||||
>
|
||||
{{ platformText(row.extractedPlatform) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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
|
||||
label="操作"
|
||||
width="300"
|
||||
fixed="right"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.token"
|
||||
link
|
||||
type="info"
|
||||
:loading="probeLoadingId === row.id"
|
||||
@click="handleProbeToken(row)"
|
||||
>检测</el-button
|
||||
>
|
||||
<el-button link type="primary" @click="openDetail(row)"
|
||||
>详情</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="!row.extractedAt && !row.extracted"
|
||||
link
|
||||
type="warning"
|
||||
@click="openExtractByRow(row)"
|
||||
>提取</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="row.extracted"
|
||||
link
|
||||
type="success"
|
||||
@click="copyCardInfo(row)"
|
||||
>复制</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@ -1003,7 +1094,9 @@ async function handleBatchProbe() {
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
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]"
|
||||
:total="total"
|
||||
/>
|
||||
@ -1055,7 +1148,9 @@ async function handleBatchProbe() {
|
||||
destroy-on-close
|
||||
>
|
||||
<el-alert type="info" :closable="false" style="margin-bottom: 12px">
|
||||
将对已选的 <strong>{{ selectedRows.length }}</strong> 条记录执行提取并标记为已提取。
|
||||
将对已选的
|
||||
<strong>{{ selectedRows.length }}</strong>
|
||||
条记录执行提取并标记为已提取。
|
||||
</el-alert>
|
||||
<el-form label-width="84px">
|
||||
<el-form-item label="提取平台">
|
||||
@ -1079,7 +1174,11 @@ async function handleBatchProbe() {
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<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>
|
||||
</template>
|
||||
@ -1299,7 +1398,6 @@ async function handleBatchProbe() {
|
||||
gap: 12px;
|
||||
padding: 8px 12px 12px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* 接口说明抽屉 */
|
||||
@ -1421,10 +1519,13 @@ async function handleBatchProbe() {
|
||||
}
|
||||
.cursor-probe-dialog .cursor-probe-result {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.cursor-probe-dialog .cursor-expire-result {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user