From c0f70823a97358452ffa192980ea1b110c3f25b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com> Date: Tue, 16 Jun 2026 01:30:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E5=90=88=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/components/CommonAside.vue | 4 +- go/controllers/api_cursor_equipment.go | 503 +++++++++ go/controllers/backend_article.go | 24 +- go/controllers/platform_account_pool.go | 340 ++++--- .../platform_cursor_activation_code.go | 750 ++++++++++++++ go/controllers/platform_cursor_equipment.go | 681 +++++++++++++ go/controllers/pool_probe.go | 63 ++ .../yz_platform_cursor_activation_code.sql | 31 + go/models/cms_article.go | 263 +++-- go/models/init.go | 6 +- go/models/platform_cursor_activation_code.go | 28 + go/models/platform_cursor_equipment.go | 26 + go/models/system_tenant_domain.go | 2 +- go/pkg/tokenprobe/cursor_hi.go | 313 ++---- go/pkg/tokenprobe/probe.go | 23 +- go/routers/api/api.go | 6 + go/routers/backend/backend.go | 69 +- go/routers/platform/platform.go | 22 + platform/src/api/accountPool.js | 8 + platform/src/api/cursorActivationCode.ts | 106 ++ platform/src/api/cursorEquipment.ts | 90 ++ platform/src/components/CommonAside.vue | 40 +- .../views/accountpool/components/patch.vue | 1 - .../accountpool/cursor/components/detail.vue | 53 + .../src/views/accountpool/cursor/index.vue | 680 ++++++++----- .../src/views/cursor/activationcode/index.vue | 957 ++++++++++++++++++ .../components/activationRecords.vue | 47 +- .../cursor/equipment/components/detail.vue | 8 +- .../equipment/components/extractRecords.vue | 65 +- platform/src/views/cursor/equipment/index.vue | 68 +- platform/vite.config.js | 1 + 31 files changed, 4385 insertions(+), 893 deletions(-) create mode 100644 go/controllers/api_cursor_equipment.go create mode 100644 go/controllers/platform_cursor_activation_code.go create mode 100644 go/controllers/platform_cursor_equipment.go create mode 100644 go/controllers/pool_probe.go create mode 100644 go/docs/sql/yz_platform_cursor_activation_code.sql create mode 100644 go/models/platform_cursor_activation_code.go create mode 100644 go/models/platform_cursor_equipment.go create mode 100644 platform/src/api/cursorActivationCode.ts create mode 100644 platform/src/api/cursorEquipment.ts create mode 100644 platform/src/views/cursor/activationcode/index.vue diff --git a/backend/src/components/CommonAside.vue b/backend/src/components/CommonAside.vue index e0b2a70..243d024 100644 --- a/backend/src/components/CommonAside.vue +++ b/backend/src/components/CommonAside.vue @@ -463,7 +463,7 @@ h3 { background-color: rgba(60, 60, 60, 0.8) !important; } color: #ffffff !important; - border-left: 3px solid #4f84ff; + /* border-left: 3px solid #4f84ff; */ margin-left: -3px; .menu-icon { @@ -504,7 +504,7 @@ h3 { .el-menu-item.is-active { background: rgba(219, 148, 148, 0.8) !important; color: var(--el-color-primary-light-3) !important; - border-left-color: var(--el-color-primary); + /* border-left-color: var(--el-color-primary); */ .menu-icon { color: var(--el-color-primary); diff --git a/go/controllers/api_cursor_equipment.go b/go/controllers/api_cursor_equipment.go new file mode 100644 index 0000000..d0e00e7 --- /dev/null +++ b/go/controllers/api_cursor_equipment.go @@ -0,0 +1,503 @@ +package controllers + +import ( + "encoding/json" + "strings" + "time" + + "server/models" + + "github.com/beego/beego/v2/client/orm" + beego "github.com/beego/beego/v2/server/web" +) + +// ApiCursorEquipmentController 开放接口:登录器上报 Cursor 设备信息(无需登录) +type ApiCursorEquipmentController struct { + beego.Controller +} + +type cursorEquipmentReportPayload struct { + DeviceInfo string `json:"deviceInfo"` + DeviceInfoSnake string `json:"device_info"` + MachineCode string `json:"machineCode"` + MachineCodeSnake string `json:"machine_code"` + Status *int8 `json:"status"` + System string `json:"system"` + Version string `json:"version"` + BindAccount string `json:"bindAccount"` + BindAccountSnake string `json:"bind_account"` + OwnerUserID *uint64 `json:"ownerUserId"` + OwnerUserIDSnake *uint64 `json:"owner_user_id"` + OwnerUserName string `json:"ownerUserName"` + OwnerUserNameSnake string `json:"owner_user_name"` + ActivationTime string `json:"activationTime"` + ActivationTimeSnake string `json:"activation_time"` + ExpireTime string `json:"expireTime"` + ExpireTimeSnake string `json:"expire_time"` + Remark string `json:"remark"` +} + +type cursorEquipmentActivatePayload struct { + Code string `json:"code"` + ActivationCode string `json:"activationCode"` + ActivationCodeSnake string `json:"activation_code"` + DeviceInfo string `json:"deviceInfo"` + DeviceInfoSnake string `json:"device_info"` + MachineCode string `json:"machineCode"` + MachineCodeSnake string `json:"machine_code"` + System string `json:"system"` + Version string `json:"version"` + BindAccount string `json:"bindAccount"` + BindAccountSnake string `json:"bind_account"` + OwnerUserID *uint64 `json:"ownerUserId"` + OwnerUserIDSnake *uint64 `json:"owner_user_id"` + OwnerUserName string `json:"ownerUserName"` + OwnerUserNameSnake string `json:"owner_user_name"` + Remark string `json:"remark"` +} + +func cursorFirstNonEmpty(values ...string) string { + for _, v := range values { + if s := strings.TrimSpace(v); s != "" { + return s + } + } + return "" +} + +func cursorStringPtr(value string) *string { + value = strings.TrimSpace(value) + if value == "" { + return nil + } + return &value +} + +func cursorParseTimePtr(value string) *time.Time { + value = strings.TrimSpace(value) + if value == "" { + return nil + } + + layouts := []string{ + time.RFC3339, + "2006-01-02 15:04:05", + "2006-01-02 15:04", + "2006-01-02", + } + for _, layout := range layouts { + if t, err := time.ParseInLocation(layout, value, time.Local); err == nil { + return &t + } + } + return nil +} + +func cursorValidStatus(status int8) bool { + return status == 0 || status == 1 || status == 2 || status == 3 +} + +func (c *ApiCursorEquipmentController) jsonResult(code int, msg string, data interface{}) { + resp := map[string]interface{}{"code": code, "msg": msg} + if data != nil { + resp["data"] = data + } + c.Data["json"] = resp + _ = c.ServeJSON() +} + +// Report POST /api/cursor/equipment/report +// +// JSON 示例: +// +// { +// "machineCode": "ABC-123", +// "deviceInfo": "CPU/RAM/磁盘等设备信息", +// "system": "Windows", +// "version": "1.0.0", +// "bindAccount": "user@example.com", +// "ownerUserId": 1, +// "ownerUserName": "张三", +// "activationTime": "2026-06-15 22:00:00", +// "expireTime": "2026-07-15 22:00:00", +// "remark": "登录器上报" +// } +// +// 兼容 snake_case 字段,例如 machine_code、device_info、bind_account。 +func (c *ApiCursorEquipmentController) Report() { + var p cursorEquipmentReportPayload + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &p); err != nil { + c.jsonResult(400, "参数错误", nil) + return + } + + machineCode := cursorFirstNonEmpty(p.MachineCode, p.MachineCodeSnake) + if machineCode == "" { + c.jsonResult(400, "缺少参数 machineCode/machine_code(机器码)", nil) + return + } + if len(machineCode) > 128 { + c.jsonResult(400, "机器码长度不能超过 128 个字符", nil) + return + } + + status := int8(0) + statusProvided := p.Status != nil + if statusProvided { + status = *p.Status + if !cursorValidStatus(status) { + c.jsonResult(400, "状态不合法,支持:0 未激活、1 激活中、2 已过期、3 已禁用", nil) + return + } + } + + deviceInfo := cursorFirstNonEmpty(p.DeviceInfo, p.DeviceInfoSnake) + system := cursorFirstNonEmpty(p.System) + version := cursorFirstNonEmpty(p.Version) + bindAccount := cursorFirstNonEmpty(p.BindAccount, p.BindAccountSnake) + ownerUserID := p.OwnerUserID + if ownerUserID == nil { + ownerUserID = p.OwnerUserIDSnake + } + ownerUserName := cursorFirstNonEmpty(p.OwnerUserName, p.OwnerUserNameSnake) + activationTime := cursorParseTimePtr(cursorFirstNonEmpty(p.ActivationTime, p.ActivationTimeSnake)) + expireTime := cursorParseTimePtr(cursorFirstNonEmpty(p.ExpireTime, p.ExpireTimeSnake)) + remark := cursorFirstNonEmpty(p.Remark) + + now := time.Now() + var row models.PlatformCursorEquipment + err := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("machine_code", machineCode). + Filter("delete_time__isnull", true). + One(&row) + + created := false + if err == orm.ErrNoRows { + row = models.PlatformCursorEquipment{ + MachineCode: machineCode, + Status: status, + DeviceInfo: cursorStringPtr(deviceInfo), + System: cursorStringPtr(system), + Version: cursorStringPtr(version), + BindAccount: cursorStringPtr(bindAccount), + OwnerUserID: ownerUserID, + OwnerUserName: cursorStringPtr(ownerUserName), + ActivationTime: activationTime, + ExpireTime: expireTime, + Remark: cursorStringPtr(remark), + CreateTime: now, + } + id, insertErr := models.Orm.Insert(&row) + if insertErr != nil { + c.jsonResult(500, "设备信息保存失败", nil) + return + } + row.ID = uint64(id) + created = true + } else if err != nil { + c.jsonResult(500, "设备信息查询失败", nil) + return + } else { + update := map[string]interface{}{ + "device_info": cursorStringPtr(deviceInfo), + "system": cursorStringPtr(system), + "version": cursorStringPtr(version), + "bind_account": cursorStringPtr(bindAccount), + "owner_user_id": ownerUserID, + "owner_user_name": cursorStringPtr(ownerUserName), + "activation_time": activationTime, + "expire_time": expireTime, + "remark": cursorStringPtr(remark), + "update_time": now, + } + if statusProvided { + update["status"] = status + row.Status = status + } + + if _, updateErr := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", row.ID). + Update(update); updateErr != nil { + c.jsonResult(500, "设备信息更新失败", nil) + return + } + + row.DeviceInfo = cursorStringPtr(deviceInfo) + row.System = cursorStringPtr(system) + row.Version = cursorStringPtr(version) + row.BindAccount = cursorStringPtr(bindAccount) + row.OwnerUserID = ownerUserID + row.OwnerUserName = cursorStringPtr(ownerUserName) + row.ActivationTime = activationTime + row.ExpireTime = expireTime + row.Remark = cursorStringPtr(remark) + row.UpdateTime = &now + } + + c.jsonResult(200, "success", map[string]interface{}{ + "id": row.ID, + "machineCode": row.MachineCode, + "status": row.Status, + "created": created, + }) +} + +// ActivateByCode POST /api/cursor/equipment/activateByCode +// +// 设备端使用激活码激活/续期 Cursor 设备(无需登录)。 +// +// JSON 示例: +// +// { +// "activationCode": "CUR-XXXXXXXX", +// "machineCode": "ABC-123", +// "deviceInfo": "CPU/RAM/磁盘等设备信息", +// "system": "Windows", +// "version": "1.0.0", +// "bindAccount": "user@example.com", +// "ownerUserId": 1, +// "ownerUserName": "张三", +// "remark": "登录器激活" +// } +// +// 兼容字段: +// - 激活码:activationCode / activation_code / code +// - 机器码:machineCode / machine_code +// - 设备信息:deviceInfo / device_info +// - 绑定账号:bindAccount / bind_account +func (c *ApiCursorEquipmentController) ActivateByCode() { + var p cursorEquipmentActivatePayload + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &p); err != nil { + c.jsonResult(400, "参数错误", nil) + return + } + + code := cursorFirstNonEmpty(p.ActivationCode, p.ActivationCodeSnake, p.Code) + if code == "" { + c.jsonResult(400, "缺少参数 activationCode/activation_code/code(激活码)", nil) + return + } + if len(code) > 128 { + c.jsonResult(400, "激活码长度不能超过 128 个字符", nil) + return + } + + machineCode := cursorFirstNonEmpty(p.MachineCode, p.MachineCodeSnake) + if machineCode == "" { + c.jsonResult(400, "缺少参数 machineCode/machine_code(机器码)", nil) + return + } + if len(machineCode) > 128 { + c.jsonResult(400, "机器码长度不能超过 128 个字符", nil) + return + } + + deviceInfo := cursorFirstNonEmpty(p.DeviceInfo, p.DeviceInfoSnake) + system := cursorFirstNonEmpty(p.System) + version := cursorFirstNonEmpty(p.Version) + bindAccount := cursorFirstNonEmpty(p.BindAccount, p.BindAccountSnake) + ownerUserID := p.OwnerUserID + if ownerUserID == nil { + ownerUserID = p.OwnerUserIDSnake + } + ownerUserName := cursorFirstNonEmpty(p.OwnerUserName, p.OwnerUserNameSnake) + remark := cursorFirstNonEmpty(p.Remark) + + now := time.Now() + + var activationCode models.PlatformCursorActivationCode + err := models.Orm.QueryTable(new(models.PlatformCursorActivationCode)). + Filter("code", code). + Filter("delete_time__isnull", true). + One(&activationCode) + if err == orm.ErrNoRows { + c.jsonResult(404, "激活码不存在", nil) + return + } + if err != nil { + c.jsonResult(500, "激活码查询失败", nil) + return + } + + if activationCode.Status == 3 { + c.jsonResult(403, "激活码已禁用", nil) + return + } + if activationCode.Status == 2 || (activationCode.ExpiredAt != nil && activationCode.ExpiredAt.Before(now)) { + _, _ = models.Orm.QueryTable(new(models.PlatformCursorActivationCode)). + Filter("id", activationCode.ID). + Update(map[string]interface{}{"status": int8(2), "update_time": now}) + c.jsonResult(410, "激活码已过期", nil) + return + } + + if activationCode.Status == 1 { + if activationCode.MachineCode == nil || strings.TrimSpace(*activationCode.MachineCode) != machineCode { + c.jsonResult(409, "激活码已被其他设备使用", nil) + return + } + + if activationCode.ExpiredAt != nil && activationCode.ExpiredAt.Before(now) { + _, _ = models.Orm.QueryTable(new(models.PlatformCursorActivationCode)). + Filter("id", activationCode.ID). + Update(map[string]interface{}{"status": int8(2), "update_time": now}) + c.jsonResult(410, "激活码已过期", nil) + return + } + + c.jsonResult(200, "success", map[string]interface{}{ + "activated": true, + "reused": true, + "activationId": activationCode.ID, + "deviceId": activationCode.BindDeviceID, + "machineCode": machineCode, + "status": 1, + "durationDays": activationCode.DurationDays, + "activatedAt": activationCode.ActivatedAt, + "expireTime": activationCode.ExpiredAt, + "expiredAt": activationCode.ExpiredAt, + }) + return + } + + var device models.PlatformCursorEquipment + deviceErr := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("machine_code", machineCode). + Filter("delete_time__isnull", true). + One(&device) + + created := false + if deviceErr != nil && deviceErr != orm.ErrNoRows { + c.jsonResult(500, "设备信息查询失败", nil) + return + } + if deviceErr == nil && device.Status == 3 { + c.jsonResult(403, "设备已禁用,无法激活", nil) + return + } + + baseTime := now + if deviceErr == nil && device.ExpireTime != nil && device.ExpireTime.After(now) { + baseTime = *device.ExpireTime + } + + var expireTime *time.Time + if activationCode.DurationDays > 0 { + t := baseTime.AddDate(0, 0, activationCode.DurationDays) + expireTime = &t + } + + txOrm, err := models.Orm.Begin() + if err != nil { + c.jsonResult(500, "开启事务失败", nil) + return + } + + rollback := true + defer func() { + if rollback { + _ = txOrm.Rollback() + } + }() + + if deviceErr == orm.ErrNoRows { + device = models.PlatformCursorEquipment{ + MachineCode: machineCode, + Status: 1, + DeviceInfo: cursorStringPtr(deviceInfo), + System: cursorStringPtr(system), + Version: cursorStringPtr(version), + BindAccount: cursorStringPtr(bindAccount), + OwnerUserID: ownerUserID, + OwnerUserName: cursorStringPtr(ownerUserName), + ActivationTime: &now, + ExpireTime: expireTime, + Remark: cursorStringPtr(remark), + CreateTime: now, + } + id, insertErr := txOrm.Insert(&device) + if insertErr != nil { + c.jsonResult(500, "设备信息保存失败", nil) + return + } + device.ID = uint64(id) + created = true + } else { + if bindAccount == "" && device.BindAccount != nil { + bindAccount = *device.BindAccount + } + if ownerUserID == nil { + ownerUserID = device.OwnerUserID + } + if ownerUserName == "" && device.OwnerUserName != nil { + ownerUserName = *device.OwnerUserName + } + + _, updateErr := txOrm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", device.ID). + Update(map[string]interface{}{ + "device_info": cursorStringPtr(deviceInfo), + "system": cursorStringPtr(system), + "version": cursorStringPtr(version), + "bind_account": cursorStringPtr(bindAccount), + "owner_user_id": ownerUserID, + "owner_user_name": cursorStringPtr(ownerUserName), + "activation_time": now, + "expire_time": expireTime, + "status": int8(1), + "remark": cursorStringPtr(remark), + "update_time": now, + }) + if updateErr != nil { + c.jsonResult(500, "设备信息更新失败", nil) + return + } + } + + codeUpdateCount, updateCodeErr := txOrm.QueryTable(new(models.PlatformCursorActivationCode)). + Filter("id", activationCode.ID). + Filter("status", int8(0)). + Filter("delete_time__isnull", true). + Update(map[string]interface{}{ + "status": int8(1), + "bind_account": cursorStringPtr(bindAccount), + "bind_device_id": device.ID, + "machine_code": machineCode, + "device_info": cursorStringPtr(deviceInfo), + "owner_user_id": ownerUserID, + "owner_user_name": cursorStringPtr(ownerUserName), + "activated_at": now, + "expired_at": expireTime, + "remark": cursorStringPtr(remark), + "update_time": now, + }) + if updateCodeErr != nil { + c.jsonResult(500, "激活码绑定失败", nil) + return + } + if codeUpdateCount == 0 { + c.jsonResult(409, "激活码状态已变化,请重新查询后再试", nil) + return + } + + if err := txOrm.Commit(); err != nil { + c.jsonResult(500, "提交事务失败", nil) + return + } + rollback = false + + c.jsonResult(200, "success", map[string]interface{}{ + "activated": true, + "reused": false, + "created": created, + "activationId": activationCode.ID, + "deviceId": device.ID, + "machineCode": machineCode, + "status": 1, + "durationDays": activationCode.DurationDays, + "activationAt": now, + "activatedAt": now, + "expireTime": expireTime, + "expiredAt": expireTime, + }) +} diff --git a/go/controllers/backend_article.go b/go/controllers/backend_article.go index 832a024..7c3b7a6 100644 --- a/go/controllers/backend_article.go +++ b/go/controllers/backend_article.go @@ -287,16 +287,16 @@ func (c *BackendArticleController) Detail() { } 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"` + 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 @@ -593,8 +593,8 @@ func (c *BackendArticleController) Unpublish() { 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) } +func (c *BackendArticleController) Top() { c.setArticleFlag("top", 1) } +func (c *BackendArticleController) Untop() { c.setArticleFlag("top", 0) } // List GET /backend/categories func (c *BackendArticleCategoryController) List() { diff --git a/go/controllers/platform_account_pool.go b/go/controllers/platform_account_pool.go index f23d077..264511b 100644 --- a/go/controllers/platform_account_pool.go +++ b/go/controllers/platform_account_pool.go @@ -619,115 +619,150 @@ func replenishPoolRow(c *beego.Controller, module string) { platform := payload.Platform remark := strings.TrimSpace(payload.Remark) + replenishWithProbe(c, module, payload.Type, platform, remark, now) +} + +type poolReplenishCandidate struct { + id uint64 + dataType string + token string + isUsed *int8 + row interface{} +} + +type poolReplenishFetcher func() (*poolReplenishCandidate, error) + +// replenishWithProbe 按 id 顺序补号并探测;不可用则标记 is_extracted=2 后继续下一条。 +func replenishWithProbe(c *beego.Controller, module, dataType, platform, remark string, now time.Time) { + var fetch poolReplenishFetcher switch module { case "cursor": - checkedCount := 0 - unavailableCount := 0 - - for { + fetch = func() (*poolReplenishCandidate, error) { 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 + err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)). + Filter("is_extracted", 0). + Filter("data_type", dataType). + Filter("delete_time__isnull", true). + OrderBy("id"). + One(&row) + if err != nil { + return nil, err } - - 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 + return &poolReplenishCandidate{ + id: row.ID, dataType: row.DataType, token: row.Token, isUsed: row.IsUsed, row: row, + }, nil } case "windsurf": - var row models.PlatformAccountPoolWindsurf - if err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)). - Filter("is_extracted", 0).Filter("data_type", payload.Type). - OrderBy("id").One(&row); err != nil { - poolJSONErr(c, 404, 404, "暂无可用账号") - return + fetch = func() (*poolReplenishCandidate, error) { + var row models.PlatformAccountPoolWindsurf + err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)). + Filter("is_extracted", 0). + Filter("data_type", dataType). + Filter("delete_time__isnull", true). + OrderBy("id"). + One(&row) + if err != nil { + return nil, err + } + return &poolReplenishCandidate{ + id: row.ID, dataType: row.DataType, token: row.Token, row: row, + }, nil } - if _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).Filter("id", row.ID).Update(map[string]interface{}{ - "is_extracted": int8(2), "extracted_time": now, "extracted_platform": platform, "remark": remark, - }); err != nil { - poolJSONErr(c, 500, 500, "补号失败: "+err.Error()) - return - } - row.IsExtracted = 2 - row.ExtractedTime = &now - row.ExtractedPlatform = &platform - row.Remark = remark - c.Data["json"] = map[string]interface{}{"code": 200, "msg": "补号成功", "data": row} case "krio": - var row models.PlatformAccountPoolKiro - if err := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)). - Filter("is_extracted", 0).Filter("data_type", payload.Type). - OrderBy("id").One(&row); err != nil { - poolJSONErr(c, 404, 404, "暂无可用账号") - return + fetch = func() (*poolReplenishCandidate, error) { + var row models.PlatformAccountPoolKiro + err := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)). + Filter("is_extracted", 0). + Filter("data_type", dataType). + Filter("delete_time__isnull", true). + OrderBy("id"). + One(&row) + if err != nil { + return nil, err + } + return &poolReplenishCandidate{ + id: row.ID, dataType: row.DataType, token: row.Token, row: row, + }, nil } - if _, err = models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).Filter("id", row.ID).Update(map[string]interface{}{ - "is_extracted": int8(2), "extracted_time": now, "extracted_platform": platform, "remark": remark, - }); err != nil { - poolJSONErr(c, 500, 500, "补号失败: "+err.Error()) - return - } - row.IsExtracted = 2 - row.ExtractedTime = &now - row.ExtractedPlatform = &platform - row.Remark = remark - c.Data["json"] = map[string]interface{}{"code": 200, "msg": "补号成功", "data": row} default: poolJSONErr(c, 400, 400, "无效模块") return } - _ = c.ServeJSON() + + tableName := poolTableName(module) + if tableName == "" { + poolJSONErr(c, 400, 400, "无效模块") + return + } + + for { + candidate, err := fetch() + if err != nil { + if err == orm.ErrNoRows { + poolJSONErr(c, 404, 404, "暂无可用账号") + } else { + poolJSONErr(c, 500, 500, "查询失败") + } + return + } + + updateFields := map[string]interface{}{ + "is_extracted": int8(2), + "extracted_time": now, + "extracted_platform": platform, + "remark": remark, + "update_time": now, + } + if _, err = models.Orm.QueryTable(tableName). + Filter("id", candidate.id). + Update(updateFields); err != nil { + poolJSONErr(c, 500, 500, "补号失败: "+err.Error()) + return + } + + if known, available := poolIsUsedAvailable(candidate.isUsed); known { + if !available { + continue + } + } else if !poolProbeToken(module, candidate.dataType, candidate.token, candidate.id) { + continue + } + + data := replenishApplyResponse(candidate.row, platform, remark, now) + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "补号成功", "data": data} + _ = c.ServeJSON() + return + } +} + +func replenishApplyResponse(row interface{}, platform, remark string, now time.Time) interface{} { + pf := platform + switch r := row.(type) { + case models.PlatformAccountPoolCursor: + r.IsExtracted = 2 + r.ExtractedTime = &now + r.ExtractedPlatform = &pf + r.Remark = remark + if r.IsUsed == nil || *r.IsUsed != 1 { + used := int8(1) + r.IsUsed = &used + } + return r + case models.PlatformAccountPoolWindsurf: + r.IsExtracted = 2 + r.ExtractedTime = &now + r.ExtractedPlatform = &pf + r.Remark = remark + return r + case models.PlatformAccountPoolKiro: + r.IsExtracted = 2 + r.ExtractedTime = &now + r.ExtractedPlatform = &pf + r.Remark = remark + return r + default: + return row + } } func updatePoolRemark(c *beego.Controller, module string) { @@ -843,6 +878,54 @@ func setPoolUnavailable(c *beego.Controller, module string) { _ = c.ServeJSON() } +func updatePoolUsable(c *beego.Controller, module string) { + if _, err := requirePlatformAuth(c); err != nil { + poolJSONErr(c, 401, 401, err.Error()) + return + } + if module != "cursor" { + poolJSONErr(c, 400, 400, "该模块不支持可用状态修改") + return + } + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + poolJSONErr(c, 400, 400, "参数错误") + return + } + var payload struct { + ID uint64 `json:"id"` + Usable int `json:"usable"` + } + if err := json.Unmarshal(raw, &payload); err != nil || payload.ID == 0 { + poolJSONErr(c, 400, 400, "参数错误") + return + } + if payload.Usable != 0 && payload.Usable != 1 { + poolJSONErr(c, 400, 400, "可用状态参数错误") + return + } + + now := time.Now() + updated, err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).Filter("id", payload.ID).Update(orm.Params{ + "is_used": int8(payload.Usable), + "update_time": now, + }) + if err != nil { + poolJSONErr(c, 500, 500, "可用状态更新失败: "+err.Error()) + return + } + if updated == 0 { + poolJSONErr(c, 404, 404, "记录不存在") + return + } + msg := "已标记不可用" + if payload.Usable == 1 { + msg = "已标记可用" + } + c.Data["json"] = map[string]interface{}{"code": 200, "msg": msg} + _ = c.ServeJSON() +} + func updatePoolPlatform(c *beego.Controller, module string) { if _, err := requirePlatformAuth(c); err != nil { poolJSONErr(c, 401, 401, err.Error()) @@ -1019,12 +1102,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 { - isUsed := int8(0) + var isUsed int8 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, @@ -1041,62 +1124,6 @@ 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") } @@ -1109,6 +1136,9 @@ func (c *PlatformAccountPoolCursorController) UpdateRemark() { func (c *PlatformAccountPoolCursorController) SetUnavailable() { setPoolUnavailable(&c.Controller, "cursor") } +func (c *PlatformAccountPoolCursorController) UpdateUsable() { + updatePoolUsable(&c.Controller, "cursor") +} func (c *PlatformAccountPoolCursorController) UpdatePlatform() { updatePoolPlatform(&c.Controller, "cursor") } diff --git a/go/controllers/platform_cursor_activation_code.go b/go/controllers/platform_cursor_activation_code.go new file mode 100644 index 0000000..110f0f6 --- /dev/null +++ b/go/controllers/platform_cursor_activation_code.go @@ -0,0 +1,750 @@ +package controllers + +import ( + "crypto/rand" + "encoding/csv" + "encoding/hex" + "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" +) + +// PlatformCursorActivationCodeController 平台端 Cursor 激活码管理 +type PlatformCursorActivationCodeController struct { + beego.Controller +} + +func (c *PlatformCursorActivationCodeController) platformClaims() (*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 != "platform" { + return nil, fmt.Errorf("无权访问") + } + return claims, nil +} + +func (c *PlatformCursorActivationCodeController) jsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *PlatformCursorActivationCodeController) ok(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +func cursorActivationCodeTrimPtr(value *string) *string { + if value == nil { + return nil + } + v := strings.TrimSpace(*value) + if v == "" { + return nil + } + return &v +} + +func cursorActivationCodeTimePtr(value *string) *time.Time { + if value == nil { + return nil + } + v := strings.TrimSpace(*value) + if v == "" { + return nil + } + layouts := []string{ + time.RFC3339, + "2006-01-02 15:04:05", + "2006-01-02 15:04", + "2006-01-02", + } + for _, layout := range layouts { + if t, err := time.ParseInLocation(layout, v, time.Local); err == nil { + return &t + } + } + return nil +} + +func cursorActivationCodeStatusValid(status int8) bool { + return status == 0 || status == 1 || status == 2 || status == 3 +} + +func cursorActivationCodeTypeName(cardType int) string { + switch cardType { + case 1: + return "天卡" + case 7: + return "周卡" + case 30: + return "月卡" + case 90: + return "季卡" + case 365: + return "年卡" + case 0: + return "自定义" + default: + return fmt.Sprintf("%d天", cardType) + } +} + +func (c *PlatformCursorActivationCodeController) rowToMap(row *models.PlatformCursorActivationCode) map[string]interface{} { + bindStatus := 0 + if row.BindAccount != nil || row.BindDeviceID != nil || row.MachineCode != nil { + bindStatus = 1 + } + + return map[string]interface{}{ + "id": row.ID, + "code": row.Code, + "type": row.Type, + "typeName": cursorActivationCodeTypeName(row.Type), + "status": row.Status, + "durationDays": row.DurationDays, + "bindAccount": row.BindAccount, + "bindDeviceId": row.BindDeviceID, + "bindStatus": bindStatus, + "machineCode": row.MachineCode, + "deviceInfo": row.DeviceInfo, + "ownerUserId": row.OwnerUserID, + "ownerUserName": row.OwnerUserName, + "activatedAt": row.ActivatedAt, + "expiredAt": row.ExpiredAt, + "createdAt": row.CreateTime, + "updatedAt": row.UpdateTime, + "createTime": row.CreateTime, + "updateTime": row.UpdateTime, + "remark": row.Remark, + } +} + +func (c *PlatformCursorActivationCodeController) filteredQuery() orm.QuerySeter { + keyword := strings.TrimSpace(c.GetString("keyword")) + statusText := strings.TrimSpace(c.GetString("status")) + typeText := strings.TrimSpace(c.GetString("type")) + bindStatusText := strings.TrimSpace(c.GetString("bindStatus")) + + qs := models.Orm.QueryTable(new(models.PlatformCursorActivationCode)).Filter("delete_time__isnull", true) + + if keyword != "" { + cond := orm.NewCondition(). + Or("code__icontains", keyword). + Or("bind_account__icontains", keyword). + Or("machine_code__icontains", keyword). + Or("device_info__icontains", keyword). + Or("owner_user_name__icontains", keyword). + Or("remark__icontains", keyword) + qs = qs.SetCond(cond) + qs = qs.Filter("delete_time__isnull", true) + } + + if statusText != "" { + status, err := strconv.ParseInt(statusText, 10, 8) + if err == nil && cursorActivationCodeStatusValid(int8(status)) { + qs = qs.Filter("status", int8(status)) + } + } + + if typeText != "" { + cardType, err := strconv.Atoi(typeText) + if err == nil { + qs = qs.Filter("type", cardType) + } + } + + if bindStatusText != "" { + bindStatus, err := strconv.Atoi(bindStatusText) + if err == nil { + if bindStatus == 0 { + qs = qs.Filter("bind_account__isnull", true).Filter("bind_device_id__isnull", true).Filter("machine_code__isnull", true) + } else if bindStatus == 1 { + cond := orm.NewCondition(). + Or("bind_account__isnull", false). + Or("bind_device_id__isnull", false). + Or("machine_code__isnull", false) + qs = qs.SetCond(cond) + qs = qs.Filter("delete_time__isnull", true) + if statusText != "" { + status, err := strconv.ParseInt(statusText, 10, 8) + if err == nil && cursorActivationCodeStatusValid(int8(status)) { + qs = qs.Filter("status", int8(status)) + } + } + if typeText != "" { + cardType, err := strconv.Atoi(typeText) + if err == nil { + qs = qs.Filter("type", cardType) + } + } + } + } + } + + return qs +} + +// List GET /platform/cursor/activationcode/list +func (c *PlatformCursorActivationCodeController) List() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + if pageSize > 200 { + pageSize = 200 + } + + qs := c.filteredQuery() + total, _ := qs.Count() + + var rows []models.PlatformCursorActivationCode + _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows) + if err != nil { + c.jsonErr(500, 500, "获取激活码列表失败: "+err.Error()) + return + } + + list := make([]map[string]interface{}, 0, len(rows)) + for i := range rows { + list = append(list, c.rowToMap(&rows[i])) + } + + c.ok(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }) +} + +// Detail GET /platform/cursor/activationcode/detail/:id +func (c *PlatformCursorActivationCodeController) Detail() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.jsonErr(400, 400, "无效ID") + return + } + + var row models.PlatformCursorActivationCode + err = models.Orm.QueryTable(new(models.PlatformCursorActivationCode)). + Filter("id", id). + Filter("delete_time__isnull", true). + One(&row) + if err != nil { + c.jsonErr(404, 404, "激活码不存在") + return + } + + c.ok(c.rowToMap(&row)) +} + +type platformCursorActivationCodePayload struct { + ID *uint64 `json:"id"` + Code *string `json:"code"` + Type *int `json:"type"` + Status *int8 `json:"status"` + DurationDays *int `json:"durationDays"` + BindAccount *string `json:"bindAccount"` + BindDeviceID *uint64 `json:"bindDeviceId"` + OwnerUserID *uint64 `json:"ownerUserId"` + OwnerUserName *string `json:"ownerUserName"` + ActivatedAt *string `json:"activatedAt"` + ExpiredAt *string `json:"expiredAt"` + Remark *string `json:"remark"` +} + +func (c *PlatformCursorActivationCodeController) readPayload() (*platformCursorActivationCodePayload, error) { + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p platformCursorActivationCodePayload + if err := json.Unmarshal(body, &p); err != nil { + return nil, err + } + return &p, nil +} + +func (c *PlatformCursorActivationCodeController) fillDeviceSnapshot(up map[string]interface{}, bindDeviceID *uint64) { + if bindDeviceID == nil || *bindDeviceID == 0 { + up["bind_device_id"] = nil + up["machine_code"] = nil + up["device_info"] = nil + return + } + + var device models.PlatformCursorEquipment + err := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", *bindDeviceID). + Filter("delete_time__isnull", true). + One(&device) + if err == nil { + up["bind_device_id"] = *bindDeviceID + up["machine_code"] = device.MachineCode + up["device_info"] = device.DeviceInfo + return + } + + up["bind_device_id"] = *bindDeviceID +} + +func (c *PlatformCursorActivationCodeController) payloadToUpdateMap(p *platformCursorActivationCodePayload, includeCode bool) (map[string]interface{}, error) { + up := map[string]interface{}{} + + if includeCode { + if p.Code == nil || strings.TrimSpace(*p.Code) == "" { + return nil, fmt.Errorf("激活码不能为空") + } + up["code"] = strings.TrimSpace(*p.Code) + } else if p.Code != nil { + if strings.TrimSpace(*p.Code) == "" { + return nil, fmt.Errorf("激活码不能为空") + } + up["code"] = strings.TrimSpace(*p.Code) + } + + if p.Type != nil { + if *p.Type < 0 { + return nil, fmt.Errorf("卡密类型不合法") + } + up["type"] = *p.Type + } + if p.Status != nil { + if !cursorActivationCodeStatusValid(*p.Status) { + return nil, fmt.Errorf("状态不合法,支持:0 未使用、1 已使用、2 已过期、3 已禁用") + } + up["status"] = *p.Status + } + if p.DurationDays != nil { + if *p.DurationDays < 0 || *p.DurationDays > 9999 { + return nil, fmt.Errorf("有效天数范围为 0-9999") + } + up["duration_days"] = *p.DurationDays + } + if p.BindAccount != nil { + up["bind_account"] = cursorActivationCodeTrimPtr(p.BindAccount) + } + if p.BindDeviceID != nil { + c.fillDeviceSnapshot(up, p.BindDeviceID) + } + if p.OwnerUserID != nil { + if *p.OwnerUserID == 0 { + up["owner_user_id"] = nil + } else { + up["owner_user_id"] = *p.OwnerUserID + } + } + if p.OwnerUserName != nil { + up["owner_user_name"] = cursorActivationCodeTrimPtr(p.OwnerUserName) + } + if p.ActivatedAt != nil { + up["activated_at"] = cursorActivationCodeTimePtr(p.ActivatedAt) + } + if p.ExpiredAt != nil { + up["expired_at"] = cursorActivationCodeTimePtr(p.ExpiredAt) + } + if p.Remark != nil { + up["remark"] = cursorActivationCodeTrimPtr(p.Remark) + } + + return up, nil +} + +// Add POST /platform/cursor/activationcode/add +func (c *PlatformCursorActivationCodeController) Add() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + p, err := c.readPayload() + if err != nil { + c.jsonErr(400, 400, "参数错误") + return + } + + up, err := c.payloadToUpdateMap(p, true) + if err != nil { + c.jsonErr(400, 400, err.Error()) + return + } + + row := models.PlatformCursorActivationCode{ + Code: up["code"].(string), + Type: 30, + Status: 0, + DurationDays: 30, + BindAccount: cursorActivationCodeTrimPtr(p.BindAccount), + BindDeviceID: p.BindDeviceID, + OwnerUserID: p.OwnerUserID, + OwnerUserName: cursorActivationCodeTrimPtr(p.OwnerUserName), + ActivatedAt: cursorActivationCodeTimePtr(p.ActivatedAt), + ExpiredAt: cursorActivationCodeTimePtr(p.ExpiredAt), + Remark: cursorActivationCodeTrimPtr(p.Remark), + CreateTime: time.Now(), + } + + if p.Type != nil { + row.Type = *p.Type + } + if p.Status != nil { + row.Status = *p.Status + } + if p.DurationDays != nil { + row.DurationDays = *p.DurationDays + } + if row.BindDeviceID != nil && *row.BindDeviceID == 0 { + row.BindDeviceID = nil + } + if row.OwnerUserID != nil && *row.OwnerUserID == 0 { + row.OwnerUserID = nil + } + if row.BindDeviceID != nil { + var device models.PlatformCursorEquipment + if err := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", *row.BindDeviceID). + Filter("delete_time__isnull", true). + One(&device); err == nil { + row.MachineCode = &device.MachineCode + row.DeviceInfo = device.DeviceInfo + } + } + + id, err := models.Orm.Insert(&row) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "duplicate") { + c.jsonErr(400, 400, "激活码已存在") + return + } + c.jsonErr(500, 500, "新增激活码失败: "+err.Error()) + return + } + + c.ok(map[string]interface{}{"id": id}) +} + +// Update POST /platform/cursor/activationcode/update +func (c *PlatformCursorActivationCodeController) Update() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + p, err := c.readPayload() + if err != nil { + c.jsonErr(400, 400, "参数错误") + return + } + if p.ID == nil || *p.ID == 0 { + c.jsonErr(400, 400, "无效ID") + return + } + + up, err := c.payloadToUpdateMap(p, false) + if err != nil { + c.jsonErr(400, 400, err.Error()) + return + } + if len(up) == 0 { + c.jsonErr(400, 400, "无更新字段") + return + } + + now := time.Now() + up["update_time"] = now + + n, err := models.Orm.QueryTable(new(models.PlatformCursorActivationCode)). + Filter("id", *p.ID). + Filter("delete_time__isnull", true). + Update(up) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "duplicate") { + c.jsonErr(400, 400, "激活码已存在") + return + } + c.jsonErr(500, 500, "更新激活码失败: "+err.Error()) + return + } + if n == 0 { + c.jsonErr(404, 404, "激活码不存在") + return + } + + c.ok(nil) +} + +// Delete POST /platform/cursor/activationcode/delete/:id +func (c *PlatformCursorActivationCodeController) Delete() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.jsonErr(400, 400, "无效ID") + return + } + + now := time.Now() + n, err := models.Orm.QueryTable(new(models.PlatformCursorActivationCode)). + Filter("id", id). + Filter("delete_time__isnull", true). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + c.jsonErr(500, 500, "删除激活码失败: "+err.Error()) + return + } + if n == 0 { + c.jsonErr(404, 404, "激活码不存在") + return + } + + c.ok(nil) +} + +type platformCursorActivationCodeGeneratePayload struct { + Count int `json:"count"` + Type int `json:"type"` + DurationDays int `json:"durationDays"` + OwnerUserID *uint64 `json:"ownerUserId"` + OwnerUserName *string `json:"ownerUserName"` + Remark *string `json:"remark"` +} + +func randomCursorActivationCode() (string, error) { + b := make([]byte, 12) + if _, err := rand.Read(b); err != nil { + return "", err + } + return "CUR-" + strings.ToUpper(hex.EncodeToString(b)), nil +} + +// Generate POST /platform/cursor/activationcode/generate +func (c *PlatformCursorActivationCodeController) Generate() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p platformCursorActivationCodeGeneratePayload + if err := json.Unmarshal(body, &p); err != nil { + c.jsonErr(400, 400, "参数错误") + return + } + + if p.Count < 1 { + p.Count = 1 + } + if p.Count > 10000 { + c.jsonErr(400, 400, "单次最多生成 10000 个激活码") + return + } + if p.Type < 0 { + c.jsonErr(400, 400, "卡密类型不合法") + return + } + if p.DurationDays < 0 || p.DurationDays > 9999 { + c.jsonErr(400, 400, "有效天数范围为 0-9999") + return + } + if p.Type == 0 && p.DurationDays == 0 { + p.DurationDays = 30 + } + if p.Type > 0 && p.DurationDays == 0 { + p.DurationDays = p.Type + } + + createdIDs := make([]int64, 0, p.Count) + codes := make([]string, 0, p.Count) + now := time.Now() + + for len(createdIDs) < p.Count { + code, err := randomCursorActivationCode() + if err != nil { + c.jsonErr(500, 500, "生成激活码失败: "+err.Error()) + return + } + + row := models.PlatformCursorActivationCode{ + Code: code, + Type: p.Type, + Status: 0, + DurationDays: p.DurationDays, + OwnerUserID: p.OwnerUserID, + OwnerUserName: cursorActivationCodeTrimPtr(p.OwnerUserName), + Remark: cursorActivationCodeTrimPtr(p.Remark), + CreateTime: now, + } + if row.OwnerUserID != nil && *row.OwnerUserID == 0 { + row.OwnerUserID = nil + } + + id, err := models.Orm.Insert(&row) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "duplicate") { + continue + } + c.jsonErr(500, 500, "生成激活码失败: "+err.Error()) + return + } + + createdIDs = append(createdIDs, id) + codes = append(codes, code) + } + + c.ok(map[string]interface{}{ + "count": len(createdIDs), + "ids": createdIDs, + "codes": codes, + }) +} + +// Enable POST /platform/cursor/activationcode/enable/:id +func (c *PlatformCursorActivationCodeController) Enable() { + c.changeStatus(0, "启用激活码失败") +} + +// Disable POST /platform/cursor/activationcode/disable/:id +func (c *PlatformCursorActivationCodeController) Disable() { + c.changeStatus(3, "禁用激活码失败") +} + +func (c *PlatformCursorActivationCodeController) changeStatus(status int8, failMsg string) { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.jsonErr(400, 400, "无效ID") + return + } + + now := time.Now() + n, err := models.Orm.QueryTable(new(models.PlatformCursorActivationCode)). + Filter("id", id). + Filter("delete_time__isnull", true). + Update(map[string]interface{}{ + "status": status, + "update_time": now, + }) + if err != nil { + c.jsonErr(500, 500, failMsg+": "+err.Error()) + return + } + if n == 0 { + c.jsonErr(404, 404, "激活码不存在") + return + } + + c.ok(nil) +} + +// Export GET /platform/cursor/activationcode/export +func (c *PlatformCursorActivationCodeController) Export() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + var rows []models.PlatformCursorActivationCode + _, err := c.filteredQuery().OrderBy("-id").Limit(50000).All(&rows) + if err != nil { + c.jsonErr(500, 500, "导出激活码失败: "+err.Error()) + return + } + + filename := fmt.Sprintf("cursor-activation-code-%s.csv", time.Now().Format("20060102150405")) + c.Ctx.Output.Header("Content-Type", "text/csv; charset=utf-8") + c.Ctx.Output.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) + + _, _ = c.Ctx.ResponseWriter.Write([]byte{0xEF, 0xBB, 0xBF}) + writer := csv.NewWriter(c.Ctx.ResponseWriter) + _ = writer.Write([]string{ + "ID", "激活码", "类型", "有效天数", "状态", "绑定账号", "绑定设备ID", "机器码", "归属用户ID", "归属用户", "激活时间", "过期时间", "创建时间", "备注", + }) + + statusText := map[int8]string{ + 0: "未使用", + 1: "已使用", + 2: "已过期", + 3: "已禁用", + } + + for i := range rows { + row := rows[i] + _ = writer.Write([]string{ + strconv.FormatUint(row.ID, 10), + row.Code, + cursorActivationCodeTypeName(row.Type), + strconv.Itoa(row.DurationDays), + statusText[row.Status], + stringPtrValue(row.BindAccount), + uint64PtrValue(row.BindDeviceID), + stringPtrValue(row.MachineCode), + uint64PtrValue(row.OwnerUserID), + stringPtrValue(row.OwnerUserName), + timePtrValue(row.ActivatedAt), + timePtrValue(row.ExpiredAt), + row.CreateTime.Format("2006-01-02 15:04:05"), + stringPtrValue(row.Remark), + }) + } + + writer.Flush() +} + +func stringPtrValue(value *string) string { + if value == nil { + return "" + } + return *value +} + +func uint64PtrValue(value *uint64) string { + if value == nil { + return "" + } + return strconv.FormatUint(*value, 10) +} + +func timePtrValue(value *time.Time) string { + if value == nil { + return "" + } + return value.Format("2006-01-02 15:04:05") +} diff --git a/go/controllers/platform_cursor_equipment.go b/go/controllers/platform_cursor_equipment.go new file mode 100644 index 0000000..dfdc68c --- /dev/null +++ b/go/controllers/platform_cursor_equipment.go @@ -0,0 +1,681 @@ +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" +) + +// PlatformCursorEquipmentController 平台端 Cursor 设备管理 +type PlatformCursorEquipmentController struct { + beego.Controller +} + +func (c *PlatformCursorEquipmentController) platformClaims() (*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 != "platform" { + return nil, fmt.Errorf("无权访问") + } + return claims, nil +} + +func (c *PlatformCursorEquipmentController) jsonErr(httpStatus, bizCode int, msg string) { + c.Ctx.Output.SetStatus(httpStatus) + c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg} + _ = c.ServeJSON() +} + +func (c *PlatformCursorEquipmentController) ok(data interface{}) { + c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} + _ = c.ServeJSON() +} + +func cursorEquipmentTrimPtr(value *string) *string { + if value == nil { + return nil + } + v := strings.TrimSpace(*value) + if v == "" { + return nil + } + return &v +} + +func cursorEquipmentTimePtr(value *string) *time.Time { + if value == nil { + return nil + } + v := strings.TrimSpace(*value) + if v == "" { + return nil + } + layouts := []string{ + time.RFC3339, + "2006-01-02 15:04:05", + "2006-01-02 15:04", + "2006-01-02", + } + for _, layout := range layouts { + if t, err := time.ParseInLocation(layout, v, time.Local); err == nil { + return &t + } + } + return nil +} + +func cursorEquipmentStatusValid(status int8) bool { + return status == 0 || status == 1 || status == 2 || status == 3 +} + +func (c *PlatformCursorEquipmentController) cursorActivationSummary(row *models.PlatformCursorEquipment) (int64, *models.PlatformCursorActivationCode) { + cond := orm.NewCondition(). + And("delete_time__isnull", true). + AndCond(orm.NewCondition(). + Or("bind_device_id", row.ID). + Or("machine_code", row.MachineCode)) + + qs := models.Orm.QueryTable(new(models.PlatformCursorActivationCode)).SetCond(cond) + count, _ := qs.Count() + + var latest models.PlatformCursorActivationCode + if err := qs.OrderBy("-activated_at", "-id").One(&latest); err != nil { + return count, nil + } + + return count, &latest +} + +func (c *PlatformCursorEquipmentController) cursorExtractSummary() (int64, *models.PlatformAccountPoolCursor) { + qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)). + Filter("delete_time__isnull", true). + Filter("is_extracted__gt", 0) + + count, _ := qs.Count() + + var latest models.PlatformAccountPoolCursor + if err := qs.OrderBy("-extracted_time", "-id").One(&latest); err != nil { + return count, nil + } + + return count, &latest +} + +func (c *PlatformCursorEquipmentController) rowToMap(row *models.PlatformCursorEquipment) map[string]interface{} { + activationCount, latestActivation := c.cursorActivationSummary(row) + extractCount, latestExtract := c.cursorExtractSummary() + + var bindActivationCode interface{} + var activationCodeId interface{} + var lastActivatedAt interface{} = row.ActivationTime + var expireTime interface{} = row.ExpireTime + var lastExtractedAt interface{} + if latestActivation != nil { + bindActivationCode = latestActivation.Code + activationCodeId = latestActivation.ID + if latestActivation.ActivatedAt != nil { + lastActivatedAt = latestActivation.ActivatedAt + } + if latestActivation.ExpiredAt != nil { + expireTime = latestActivation.ExpiredAt + } + } + if latestExtract != nil { + lastExtractedAt = latestExtract.ExtractedTime + } + + return map[string]interface{}{ + "id": row.ID, + "deviceInfo": row.DeviceInfo, + "machineCode": row.MachineCode, + "status": row.Status, + "system": row.System, + "os": row.System, + "version": row.Version, + "bindAccount": row.BindAccount, + "bindActivationCode": bindActivationCode, + "activationCode": bindActivationCode, + "activationCodeId": activationCodeId, + "ownerUserId": row.OwnerUserID, + "ownerUserName": row.OwnerUserName, + "activationTime": lastActivatedAt, + "lastActivatedAt": lastActivatedAt, + "expireTime": expireTime, + "expiredAt": expireTime, + "activationCount": activationCount, + "extractCount": extractCount, + "lastExtractedAt": lastExtractedAt, + "remark": row.Remark, + "createTime": row.CreateTime, + "updateTime": row.UpdateTime, + } +} + +// List GET /platform/cursor/equipment/list +func (c *PlatformCursorEquipmentController) List() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + if pageSize > 200 { + pageSize = 200 + } + + keyword := strings.TrimSpace(c.GetString("keyword")) + statusText := strings.TrimSpace(c.GetString("status")) + system := strings.TrimSpace(c.GetString("system")) + if system == "" { + system = strings.TrimSpace(c.GetString("os")) + } + + qs := models.Orm.QueryTable(new(models.PlatformCursorEquipment)).Filter("delete_time__isnull", true) + + if keyword != "" { + cond := orm.NewCondition(). + Or("machine_code__icontains", keyword). + Or("device_info__icontains", keyword). + Or("bind_account__icontains", keyword). + Or("owner_user_name__icontains", keyword). + Or("remark__icontains", keyword) + qs = qs.SetCond(cond) + } + + if statusText != "" { + status, err := strconv.ParseInt(statusText, 10, 8) + if err == nil && cursorEquipmentStatusValid(int8(status)) { + qs = qs.Filter("status", int8(status)) + } + } + + if system != "" { + qs = qs.Filter("system__icontains", system) + } + + total, _ := qs.Count() + + var rows []models.PlatformCursorEquipment + _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows) + if err != nil { + c.jsonErr(500, 500, "获取设备列表失败: "+err.Error()) + return + } + + list := make([]map[string]interface{}, 0, len(rows)) + for i := range rows { + list = append(list, c.rowToMap(&rows[i])) + } + + c.ok(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }) +} + +// Detail GET /platform/cursor/equipment/detail/:id +func (c *PlatformCursorEquipmentController) Detail() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.jsonErr(400, 400, "无效ID") + return + } + + var row models.PlatformCursorEquipment + err = models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", id). + Filter("delete_time__isnull", true). + One(&row) + if err != nil { + c.jsonErr(404, 404, "设备不存在") + return + } + + c.ok(c.rowToMap(&row)) +} + +type platformCursorEquipmentPayload struct { + ID *uint64 `json:"id"` + DeviceInfo *string `json:"deviceInfo"` + MachineCode *string `json:"machineCode"` + Status *int8 `json:"status"` + System *string `json:"system"` + Version *string `json:"version"` + BindAccount *string `json:"bindAccount"` + OwnerUserID *uint64 `json:"ownerUserId"` + OwnerUserName *string `json:"ownerUserName"` + ActivationTime *string `json:"activationTime"` + ExpireTime *string `json:"expireTime"` + Remark *string `json:"remark"` +} + +func (c *PlatformCursorEquipmentController) readPayload() (*platformCursorEquipmentPayload, error) { + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p platformCursorEquipmentPayload + if err := json.Unmarshal(body, &p); err != nil { + return nil, err + } + return &p, nil +} + +func (c *PlatformCursorEquipmentController) payloadToUpdateMap(p *platformCursorEquipmentPayload, includeMachineCode bool) (map[string]interface{}, error) { + up := map[string]interface{}{} + + if includeMachineCode { + if p.MachineCode == nil || strings.TrimSpace(*p.MachineCode) == "" { + return nil, fmt.Errorf("机器码不能为空") + } + up["machine_code"] = strings.TrimSpace(*p.MachineCode) + } else if p.MachineCode != nil { + if strings.TrimSpace(*p.MachineCode) == "" { + return nil, fmt.Errorf("机器码不能为空") + } + up["machine_code"] = strings.TrimSpace(*p.MachineCode) + } + + if p.DeviceInfo != nil { + up["device_info"] = cursorEquipmentTrimPtr(p.DeviceInfo) + } + if p.Status != nil { + if !cursorEquipmentStatusValid(*p.Status) { + return nil, fmt.Errorf("状态不合法,支持:0 未激活、1 激活中、2 已过期、3 已禁用") + } + up["status"] = *p.Status + } + if p.System != nil { + up["system"] = cursorEquipmentTrimPtr(p.System) + } + if p.Version != nil { + up["version"] = cursorEquipmentTrimPtr(p.Version) + } + if p.BindAccount != nil { + up["bind_account"] = cursorEquipmentTrimPtr(p.BindAccount) + } + if p.OwnerUserID != nil { + if *p.OwnerUserID == 0 { + up["owner_user_id"] = nil + } else { + up["owner_user_id"] = *p.OwnerUserID + } + } + if p.OwnerUserName != nil { + up["owner_user_name"] = cursorEquipmentTrimPtr(p.OwnerUserName) + } + if p.ActivationTime != nil { + up["activation_time"] = cursorEquipmentTimePtr(p.ActivationTime) + } + if p.ExpireTime != nil { + up["expire_time"] = cursorEquipmentTimePtr(p.ExpireTime) + } + if p.Remark != nil { + up["remark"] = cursorEquipmentTrimPtr(p.Remark) + } + + return up, nil +} + +// Add POST /platform/cursor/equipment/add +func (c *PlatformCursorEquipmentController) Add() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + p, err := c.readPayload() + if err != nil { + c.jsonErr(400, 400, "参数错误") + return + } + + up, err := c.payloadToUpdateMap(p, true) + if err != nil { + c.jsonErr(400, 400, err.Error()) + return + } + + status := int8(0) + if value, ok := up["status"]; ok { + status = value.(int8) + } + + row := models.PlatformCursorEquipment{ + MachineCode: up["machine_code"].(string), + Status: status, + DeviceInfo: cursorEquipmentTrimPtr(p.DeviceInfo), + System: cursorEquipmentTrimPtr(p.System), + Version: cursorEquipmentTrimPtr(p.Version), + BindAccount: cursorEquipmentTrimPtr(p.BindAccount), + OwnerUserID: p.OwnerUserID, + OwnerUserName: cursorEquipmentTrimPtr(p.OwnerUserName), + ActivationTime: cursorEquipmentTimePtr(p.ActivationTime), + ExpireTime: cursorEquipmentTimePtr(p.ExpireTime), + Remark: cursorEquipmentTrimPtr(p.Remark), + CreateTime: time.Now(), + } + + if row.OwnerUserID != nil && *row.OwnerUserID == 0 { + row.OwnerUserID = nil + } + + id, err := models.Orm.Insert(&row) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "duplicate") { + c.jsonErr(400, 400, "机器码已存在") + return + } + c.jsonErr(500, 500, "新增设备失败: "+err.Error()) + return + } + + c.ok(map[string]interface{}{"id": id}) +} + +// Update POST /platform/cursor/equipment/update +func (c *PlatformCursorEquipmentController) Update() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + p, err := c.readPayload() + if err != nil { + c.jsonErr(400, 400, "参数错误") + return + } + if p.ID == nil || *p.ID == 0 { + c.jsonErr(400, 400, "无效ID") + return + } + + up, err := c.payloadToUpdateMap(p, false) + if err != nil { + c.jsonErr(400, 400, err.Error()) + return + } + if len(up) == 0 { + c.jsonErr(400, 400, "无更新字段") + return + } + now := time.Now() + up["update_time"] = now + + n, err := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", *p.ID). + Filter("delete_time__isnull", true). + Update(up) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "duplicate") { + c.jsonErr(400, 400, "机器码已存在") + return + } + c.jsonErr(500, 500, "更新设备失败: "+err.Error()) + return + } + if n == 0 { + c.jsonErr(404, 404, "设备不存在") + return + } + + c.ok(nil) +} + +// Delete POST /platform/cursor/equipment/delete/:id +func (c *PlatformCursorEquipmentController) Delete() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.jsonErr(400, 400, "无效ID") + return + } + + now := time.Now() + n, err := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", id). + Filter("delete_time__isnull", true). + Update(map[string]interface{}{"delete_time": now}) + if err != nil { + c.jsonErr(500, 500, "删除设备失败: "+err.Error()) + return + } + if n == 0 { + c.jsonErr(404, 404, "设备不存在") + return + } + + c.ok(nil) +} + +type platformCursorEquipmentActivatePayload struct { + ID uint64 `json:"id"` +} + +// Activate POST /platform/cursor/equipment/activate +func (c *PlatformCursorEquipmentController) Activate() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + body, _ := io.ReadAll(c.Ctx.Request.Body) + var p platformCursorEquipmentActivatePayload + if err := json.Unmarshal(body, &p); err != nil || p.ID == 0 { + c.jsonErr(400, 400, "无效ID") + return + } + + now := time.Now() + n, err := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", p.ID). + Filter("delete_time__isnull", true). + Update(map[string]interface{}{ + "status": int8(1), + "activation_time": now, + "update_time": now, + }) + if err != nil { + c.jsonErr(500, 500, "激活设备失败: "+err.Error()) + return + } + if n == 0 { + c.jsonErr(404, 404, "设备不存在") + return + } + + c.ok(nil) +} + +// ActivationRecords GET /platform/cursor/equipment/activationRecords +func (c *PlatformCursorEquipmentController) ActivationRecords() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + equipmentID, _ := c.GetUint64("equipmentId") + if equipmentID == 0 { + equipmentID, _ = c.GetUint64("id") + } + if equipmentID == 0 { + c.jsonErr(400, 400, "缺少设备ID") + return + } + + var equipment models.PlatformCursorEquipment + if err := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", equipmentID). + Filter("delete_time__isnull", true). + One(&equipment); err != nil { + c.jsonErr(404, 404, "设备不存在") + return + } + + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + if pageSize > 200 { + pageSize = 200 + } + + cond := orm.NewCondition(). + And("delete_time__isnull", true). + AndCond(orm.NewCondition(). + Or("bind_device_id", equipment.ID). + Or("machine_code", equipment.MachineCode)) + + qs := models.Orm.QueryTable(new(models.PlatformCursorActivationCode)).SetCond(cond) + total, _ := qs.Count() + + var rows []models.PlatformCursorActivationCode + if _, err := qs.OrderBy("-activated_at", "-id").Limit(pageSize, (page-1)*pageSize).All(&rows); err != nil { + c.jsonErr(500, 500, "获取激活记录失败: "+err.Error()) + return + } + + list := make([]map[string]interface{}, 0, len(rows)) + for i := range rows { + row := rows[i] + list = append(list, map[string]interface{}{ + "id": row.ID, + "code": row.Code, + "activationCode": row.Code, + "status": row.Status, + "durationDays": row.DurationDays, + "machineCode": row.MachineCode, + "deviceInfo": row.DeviceInfo, + "ownerUserId": row.OwnerUserID, + "ownerUserName": row.OwnerUserName, + "activatedAt": row.ActivatedAt, + "expiredAt": row.ExpiredAt, + "createdAt": row.CreateTime, + "remark": row.Remark, + }) + } + + c.ok(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }) +} + +// ExtractRecords GET /platform/cursor/equipment/extractRecords +func (c *PlatformCursorEquipmentController) ExtractRecords() { + if _, err := c.platformClaims(); err != nil { + c.jsonErr(401, 401, err.Error()) + return + } + + equipmentID, _ := c.GetUint64("equipmentId") + if equipmentID == 0 { + equipmentID, _ = c.GetUint64("id") + } + if equipmentID == 0 { + c.jsonErr(400, 400, "缺少设备ID") + return + } + + var equipment models.PlatformCursorEquipment + if err := models.Orm.QueryTable(new(models.PlatformCursorEquipment)). + Filter("id", equipmentID). + Filter("delete_time__isnull", true). + One(&equipment); err != nil { + c.jsonErr(404, 404, "设备不存在") + return + } + + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 20) + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + if pageSize > 200 { + pageSize = 200 + } + + qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)). + Filter("delete_time__isnull", true). + Filter("is_extracted__gt", 0) + + total, _ := qs.Count() + + var rows []models.PlatformAccountPoolCursor + if _, err := qs.OrderBy("-extracted_time", "-id").Limit(pageSize, (page-1)*pageSize).All(&rows); err != nil { + c.jsonErr(500, 500, "获取提取记录失败: "+err.Error()) + return + } + + list := make([]map[string]interface{}, 0, len(rows)) + for i := range rows { + row := rows[i] + content := buildCardResult(&row.Account, &row.Password, row.Token, row.DataType) + list = append(list, map[string]interface{}{ + "id": row.ID, + "status": row.IsExtracted, + "isExtracted": row.IsExtracted, + "platform": row.ExtractedPlatform, + "extractedPlatform": row.ExtractedPlatform, + "dataType": row.DataType, + "type": row.DataType, + "account": row.Account, + "password": row.Password, + "token": row.Token, + "content": content, + "extractedAt": row.ExtractedTime, + "createdAt": row.ExtractedTime, + "remark": row.Remark, + }) + } + + c.ok(map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }) +} diff --git a/go/controllers/pool_probe.go b/go/controllers/pool_probe.go new file mode 100644 index 0000000..be8ef84 --- /dev/null +++ b/go/controllers/pool_probe.go @@ -0,0 +1,63 @@ +package controllers + +import ( + "strings" + "time" + + "server/models" + "server/pkg/tokenprobe" +) + +func poolTableName(module string) string { + switch module { + case "cursor": + return new(models.PlatformAccountPoolCursor).TableName() + case "windsurf": + return new(models.PlatformAccountPoolWindsurf).TableName() + case "krio": + return new(models.PlatformAccountPoolKiro).TableName() + default: + return "" + } +} + +// poolNeedsTokenProbe account 类型无 Token,无需探测;tk / account_tk 需探测。 +func poolNeedsTokenProbe(dataType, token string) bool { + if strings.TrimSpace(token) == "" { + return false + } + return dataType != "account" +} + +func poolSaveCursorIsUsed(id uint64, isUsed int8) { + _, _ = models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)). + Filter("id", id). + Update(map[string]interface{}{ + "is_used": isUsed, + "update_time": time.Now(), + }) +} + +// poolProbeToken 探测 Token;cursor 模块会回写 is_used。 +func poolProbeToken(module, dataType, token string, rowID uint64) bool { + if !poolNeedsTokenProbe(dataType, token) { + return true + } + r := tokenprobe.ProbeOfficial(module, token) + if module == "cursor" && rowID > 0 { + var isUsed int8 + if r.OK { + isUsed = 1 + } + poolSaveCursorIsUsed(rowID, isUsed) + } + return r.OK +} + +// poolIsUsedAvailable 已有探测结论时:1=可用,0=不可用,nil=未探测。 +func poolIsUsedAvailable(isUsed *int8) (known bool, available bool) { + if isUsed == nil { + return false, false + } + return true, *isUsed == 1 +} diff --git a/go/docs/sql/yz_platform_cursor_activation_code.sql b/go/docs/sql/yz_platform_cursor_activation_code.sql new file mode 100644 index 0000000..d751da2 --- /dev/null +++ b/go/docs/sql/yz_platform_cursor_activation_code.sql @@ -0,0 +1,31 @@ +-- Cursor 激活码管理 +-- status: 0 未使用 1 已使用 2 已过期 3 已禁用 +-- type: 0 自定义 1 天卡 7 周卡 30 月卡 90 季卡 365 年卡 + +CREATE TABLE IF NOT EXISTS `yz_platform_cursor_activation_code` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `code` varchar(128) NOT NULL COMMENT '激活码', + `type` int NOT NULL DEFAULT 30 COMMENT '卡密类型:0自定义 1天卡 7周卡 30月卡 90季卡 365年卡', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0未使用 1已使用 2已过期 3已禁用', + `duration_days` int NOT NULL DEFAULT 30 COMMENT '有效天数', + `bind_account` varchar(128) DEFAULT NULL COMMENT '绑定账号', + `bind_device_id` bigint unsigned DEFAULT NULL COMMENT '绑定设备ID,关联 yz_platform_cursor_equipment.id', + `machine_code` varchar(128) DEFAULT NULL COMMENT '绑定设备机器码', + `device_info` varchar(1000) DEFAULT NULL COMMENT '绑定设备信息', + `owner_user_id` bigint unsigned DEFAULT NULL COMMENT '归属用户ID', + `owner_user_name` varchar(128) DEFAULT NULL COMMENT '归属用户名称', + `activated_at` datetime DEFAULT NULL COMMENT '激活时间', + `expired_at` datetime DEFAULT NULL COMMENT '过期时间', + `remark` varchar(1000) DEFAULT NULL COMMENT '备注', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_status_delete` (`status`,`delete_time`), + KEY `idx_type_status` (`type`,`status`), + KEY `idx_bind_account` (`bind_account`), + KEY `idx_bind_device_id` (`bind_device_id`), + KEY `idx_owner_user_id` (`owner_user_id`), + KEY `idx_expired_at` (`expired_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Cursor续杯激活码'; diff --git a/go/models/cms_article.go b/go/models/cms_article.go index f67903f..8ff270f 100644 --- a/go/models/cms_article.go +++ b/go/models/cms_article.go @@ -1,53 +1,24 @@ package models import ( - "fmt" - "strings" + "sync" "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 +// 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 + Tid uint64 `orm:"column(tid);default(0)" json:"tid"` + Cid uint64 `orm:"column(cid);default(0)" json:"cid"` Name string `orm:"column(name);size(100)" json:"name"` Image string `orm:"column(image);size(500);default()" json:"image"` Desc string `orm:"column(desc);size(500);default()" json:"desc"` Sort int `orm:"column(sort);default(0)" json:"sort"` Status int8 `orm:"column(status);default(1)" json:"status"` - CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` - UpdateTime *time.Time `orm:"column(update_time);type(datetime);auto_now;null" json:"update_time"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` } @@ -55,137 +26,133 @@ 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 +// CmsArticle CMS 文章 yz_cms_article +type CmsArticle struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid);default(0)" json:"tid"` + Title string `orm:"column(title);size(255)" json:"title"` + Author string `orm:"column(author);size(100);default()" json:"author"` + CateID uint64 `orm:"column(cate_id);default(0)" json:"cate_id"` + Content string `orm:"column(content);type(mediumtext);null" json:"content"` + Desc string `orm:"column(desc);size(500);default()" json:"desc"` + Image string `orm:"column(image);size(500);default()" json:"image"` + IsTrans int8 `orm:"column(is_trans);default(0)" json:"is_trans"` + TransURL *string `orm:"column(transurl);size(500);null" json:"transurl"` + Status int8 `orm:"column(status);default(0)" json:"status"` + Top int8 `orm:"column(top);default(0)" json:"top"` + Recommend int8 `orm:"column(recommend);default(0)" json:"recommend"` + Views int `orm:"column(views);default(0)" json:"views"` + Likes int `orm:"column(likes);default(0)" json:"likes"` + PublisherID *uint64 `orm:"column(publisher_id);null" json:"publisher_id"` + PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *CmsArticle) TableName() string { + return "yz_cms_article" +} + +var cmsArticleTablesOnce sync.Once + +// EnsureCmsArticleTables 首次使用时自动建表(若不存在)。 +func EnsureCmsArticleTables() error { + var err error + cmsArticleTablesOnce.Do(func() { + _, err = Orm.Raw(` +CREATE TABLE IF NOT EXISTS yz_cms_article_category ( + id bigint unsigned NOT NULL AUTO_INCREMENT, + tid bigint unsigned NOT NULL DEFAULT 0, + cid bigint unsigned NOT NULL DEFAULT 0, + name varchar(100) NOT NULL DEFAULT '', + image varchar(500) NOT NULL DEFAULT '', + ` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '', + sort int NOT NULL DEFAULT 0, + status tinyint NOT NULL DEFAULT 1, + create_time datetime NOT NULL, + update_time datetime DEFAULT NULL, + delete_time datetime DEFAULT NULL, + PRIMARY KEY (id), + KEY idx_tid_cid (tid, cid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec() + if err != nil { + return + } + _, err = Orm.Raw(` +CREATE TABLE IF NOT EXISTS yz_cms_article ( + id bigint unsigned NOT NULL AUTO_INCREMENT, + tid bigint unsigned NOT NULL DEFAULT 0, + title varchar(255) NOT NULL DEFAULT '', + author varchar(100) NOT NULL DEFAULT '', + cate_id bigint unsigned NOT NULL DEFAULT 0, + content mediumtext, + ` + "`desc`" + ` varchar(500) NOT NULL DEFAULT '', + image varchar(500) NOT NULL DEFAULT '', + is_trans tinyint NOT NULL DEFAULT 0, + transurl varchar(500) DEFAULT NULL, + status tinyint NOT NULL DEFAULT 0, + top tinyint NOT NULL DEFAULT 0, + recommend tinyint NOT NULL DEFAULT 0, + views int NOT NULL DEFAULT 0, + likes int NOT NULL DEFAULT 0, + publisher_id bigint unsigned DEFAULT NULL, + publish_time datetime DEFAULT NULL, + create_time datetime NOT NULL, + update_time datetime DEFAULT NULL, + delete_time datetime DEFAULT NULL, + PRIMARY KEY (id), + KEY idx_tid_status (tid, status), + KEY idx_cate_id (cate_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`).Exec() + }) + return err +} + +func CmsCategoryNameMap(tid uint64, ids []uint64) map[uint64]string { + out := make(map[uint64]string) + if len(ids) == 0 { + return out + } + var rows []CmsArticleCategory + _, _ = Orm.QueryTable(new(CmsArticleCategory)). + Filter("tid", tid). + Filter("id__in", ids). + Filter("delete_time__isnull", true). + All(&rows, "ID", "Name") + for _, r := range rows { + out[r.ID] = r.Name + } + return out } -// CmsFormatTime 格式化 CMS 可空时间。 func CmsFormatTime(t *time.Time) string { - if t == nil || t.IsZero() { + if t == nil { 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 - } +func CmsSimilarArticles(tid uint64, title string, limit int) ([]orm.Params, error) { if limit <= 0 { limit = 5 } - var rows []CmsArticle _, err := Orm.QueryTable(new(CmsArticle)). Filter("tid", tid). - Filter("title__icontains", title). Filter("delete_time__isnull", true). - OrderBy("-id"). + Filter("title__icontains", title). Limit(limit). - All(&rows, "id", "title", "cate_id", "status", "create_time") - if err != nil && err != orm.ErrNoRows { + All(&rows, "ID", "Title") + if err != nil { 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), + out := make([]orm.Params, 0, len(rows)) + for _, r := range rows { + out = append(out, orm.Params{ + "id": r.ID, + "title": r.Title, + "similarity": 80, }) } return out, nil diff --git a/go/models/init.go b/go/models/init.go index bcc357f..92a376a 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -56,11 +56,13 @@ func Init(_ string) { new(ComplaintCategory), new(PlatformComplaint), new(SystemSoftwareUpgrade), - new(CmsArticle), - new(CmsArticleCategory), + new(PlatformCursorEquipment), + new(PlatformCursorActivationCode), new(PlatformAccountPoolKiro), new(PlatformAccountPoolWindsurf), new(PlatformAccountPoolCursor), + new(CmsArticleCategory), + new(CmsArticle), ) // 创建全局 Ormer diff --git a/go/models/platform_cursor_activation_code.go b/go/models/platform_cursor_activation_code.go new file mode 100644 index 0000000..26ed40f --- /dev/null +++ b/go/models/platform_cursor_activation_code.go @@ -0,0 +1,28 @@ +package models + +import "time" + +// PlatformCursorActivationCode Cursor 续杯激活码 yz_platform_cursor_activation_code +type PlatformCursorActivationCode struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Code string `orm:"column(code);size(128);unique" json:"code"` + Type int `orm:"column(type);default(30)" json:"type"` + Status int8 `orm:"column(status);default(0)" json:"status"` + DurationDays int `orm:"column(duration_days);default(30)" json:"durationDays"` + BindAccount *string `orm:"column(bind_account);size(128);null" json:"bindAccount"` + BindDeviceID *uint64 `orm:"column(bind_device_id);null" json:"bindDeviceId"` + MachineCode *string `orm:"column(machine_code);size(128);null" json:"machineCode"` + DeviceInfo *string `orm:"column(device_info);size(1000);null" json:"deviceInfo"` + OwnerUserID *uint64 `orm:"column(owner_user_id);null" json:"ownerUserId"` + OwnerUserName *string `orm:"column(owner_user_name);size(128);null" json:"ownerUserName"` + ActivatedAt *time.Time `orm:"column(activated_at);type(datetime);null" json:"activatedAt"` + ExpiredAt *time.Time `orm:"column(expired_at);type(datetime);null" json:"expiredAt"` + Remark *string `orm:"column(remark);size(1000);null" json:"remark"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"` + UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"updateTime"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"deleteTime"` +} + +func (m *PlatformCursorActivationCode) TableName() string { + return "yz_platform_cursor_activation_code" +} diff --git a/go/models/platform_cursor_equipment.go b/go/models/platform_cursor_equipment.go new file mode 100644 index 0000000..a8d4289 --- /dev/null +++ b/go/models/platform_cursor_equipment.go @@ -0,0 +1,26 @@ +package models + +import "time" + +// PlatformCursorEquipment Cursor 设备管理 yz_platform_cursor_equipment +type PlatformCursorEquipment struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + DeviceInfo *string `orm:"column(device_info);size(1000);null" json:"deviceInfo"` + MachineCode string `orm:"column(machine_code);size(128);unique" json:"machineCode"` + Status int8 `orm:"column(status);default(0)" json:"status"` + System *string `orm:"column(system);size(64);null" json:"system"` + Version *string `orm:"column(version);size(64);null" json:"version"` + BindAccount *string `orm:"column(bind_account);size(128);null" json:"bindAccount"` + OwnerUserID *uint64 `orm:"column(owner_user_id);null" json:"ownerUserId"` + OwnerUserName *string `orm:"column(owner_user_name);size(128);null" json:"ownerUserName"` + ActivationTime *time.Time `orm:"column(activation_time);type(datetime);null" json:"activationTime"` + ExpireTime *time.Time `orm:"column(expire_time);type(datetime);null" json:"expireTime"` + Remark *string `orm:"column(remark);size(1000);null" json:"remark"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"` + UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"updateTime"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"deleteTime"` +} + +func (m *PlatformCursorEquipment) TableName() string { + return "yz_platform_cursor_equipment" +} diff --git a/go/models/system_tenant_domain.go b/go/models/system_tenant_domain.go index 8d5541b..fe44f64 100644 --- a/go/models/system_tenant_domain.go +++ b/go/models/system_tenant_domain.go @@ -16,5 +16,5 @@ type SystemTenantDomain struct { } func (m *SystemTenantDomain) TableName() string { - return "yz_tenant_domain" + return "yz_system_tenant_domain" } diff --git a/go/pkg/tokenprobe/cursor_hi.go b/go/pkg/tokenprobe/cursor_hi.go index d2ad382..2a9ccf6 100644 --- a/go/pkg/tokenprobe/cursor_hi.go +++ b/go/pkg/tokenprobe/cursor_hi.go @@ -10,7 +10,6 @@ import ( "io" "net/http" "os" - "regexp" "runtime" "strings" "time" @@ -23,7 +22,7 @@ import ( const ( cursorBackendURL = "https://api2.cursor.sh" cursorAgentPath = "/aiserver.v1.ChatService/StreamUnifiedChatWithTools" - cursorClientVersion = "3.6.31" + cursorClientVersion = "2.6.22" cursorHiMaxRead = 512 * 1024 // probeHiText 发往官方 Agent 的探测内容(与前端展示 probeMessage 一致) probeHiText = "hi" @@ -277,43 +276,55 @@ var cursorQuotaTipSig = []byte("Get Cursor Pro for more Agent usage, unlimited T const cursorLimitTipPrefix = "Get Cursor Pro for more Agent usage, unlimited Tab" -// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示 -// 返回 (isQuotaExhausted, message) -// isQuotaExhausted: true 表示额度用完/Token不可用,false 表示 Token 可用(可能有警告信息) -func classifyCursorRawStream(raw []byte) (isQuotaExhausted bool, message string) { +// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示(ASCII 区不区分大小写 + UTF-8 短语) +func classifyCursorRawStream(raw []byte) (blocked bool, reason string) { if len(raw) == 0 { return false, "" } - - // 额度用尽只按明确完整提示判定,避免“可用但带推广/提示文案”的 Token 被误标为已用完。 - // 用户自定义的二进制特征仍保留给部署方精确配置。 for _, sig := range cursorQuotaExhaustedSigsFromEnv() { if bytes.Contains(raw, sig) { - return true, "该TOKEN已用完(额度已耗尽)" + return true, fmt.Sprintf("流中匹配:CURSOR_QUOTA_EXHAUSTED_SIG_HEX 配置的二进制特征(%d 字节)", len(sig)) } } if bytes.Contains(raw, cursorQuotaTipSig) { - return true, "该TOKEN已用完(Get Cursor Pro for more Agent usage, unlimited Tab, and more.)" + 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" } 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, "suspicious activity") || - strings.Contains(flat, "unauthenticated") || - strings.Contains(flat, "unauthorized request") || - strings.Contains(flat, "unauthorizedrequest") || - strings.Contains(flat, "error_unauthorized") { - return true, "该TOKEN不可用(账号触发可疑活动风控/未认证,需要重新登录)" + if strings.Contains(flat, "you've hit your usage limit") { + return true, "流中匹配:you've hit your usage limit(UTF-8)" } - - // 版本过旧警告 - 这不是额度问题,Token 仍然可用 - // 返回 false,表示 Token 可用 - if strings.Contains(flat, "very old version") || strings.Contains(flat, "update to the latest version") { - return false, "Token可用,但客户端版本过旧,建议更新到最新版本" - } - return false, "" } @@ -389,29 +400,41 @@ func decodeConnectFramedBody(raw []byte) ([]byte, string, bool) { return nil, "", false } - return out.Bytes(), "", true + note := fmt.Sprintf("响应体已按 Connect 分帧解析(%d 帧", frameCount) + if compressedFrames > 0 { + note += fmt.Sprintf(",其中 %d 帧已做 gzip 解压", compressedFrames) + } + note += ")后分析" + return out.Bytes(), note, true } func decodeCursorResponseBody(raw []byte, contentEncoding string) ([]byte, string) { - if decoded, _, ok := decodeConnectFramedBody(raw); ok { - return decoded, "" + if decoded, note, ok := decodeConnectFramedBody(raw); ok { + return decoded, note } enc := strings.ToLower(strings.TrimSpace(contentEncoding)) if strings.Contains(enc, "gzip") || looksLikeGzip(raw) { decoded, err := gunzipBytes(raw) if err != nil { - return raw, "" + if strings.Contains(enc, "gzip") { + return raw, "响应头声明 gzip,但解压失败,已回退为原始字节预览" + } + return raw, "检测到 gzip 魔数,但解压失败,已回退为原始字节预览" } - return decoded, "" + if strings.Contains(enc, "gzip") { + return decoded, "响应体已按 gzip 解压后分析" + } + return decoded, "响应体虽未显式声明 Content-Encoding,但按 gzip 魔数解压后分析" } - return raw, "" + if enc != "" { + return raw, "响应头 Content-Encoding=" + enc + ",当前未额外解码,按原始字节分析" + } + return raw, "响应体未压缩或未声明压缩,且未识别为 Connect 分帧,按原始字节分析" } -// 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)" +// cursorStreamProtocol 与官方客户端一致:Connect-RPC + protobuf 体,HTTP/2 流式 +const cursorStreamProtocol = "Connect-Protocol-Version:1 + application/connect+proto,HTTP/2 二进制流(gRPC 兼容形态,非 JSON REST)" // cursorStreamNote 说明 rawPreview / ok 的含义边界(与「仅通 200」结论一致) const cursorStreamNote = `【协议】本 URL 为 Cursor 官方 Agent 流式接口,请求体为 protobuf(requestBodyPrefixHex 可见非表单/JSON)。` + @@ -461,189 +484,6 @@ 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, "") - - 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 标记时,按被二进制流切碎的 标记切分。 - finalText := "" - if idx := strings.LastIndex(cleaned, ""); idx >= 0 { - finalText = cleaned[idx+len(""):] - } else { - thinkCloseRe := regexp.MustCompile(`(?is)`) - 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 { @@ -664,7 +504,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) @@ -692,36 +532,29 @@ func probeCursorHiAgent(authToken string) Result { resp, err := cursorProbeHTTPClient.Do(req) if err != nil { - return cursorProbeResult(false, "请求失败: "+err.Error(), 0, body, nil, nil) + return cursorProbeResult(false, "请求 Cursor Agent 失败: "+err.Error(), 0, body, nil, nil) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { raw, _ := io.ReadAll(io.LimitReader(resp.Body, cursorHiMaxRead)) - decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding")) - isQuotaExhausted, msg := classifyCursorRawStream(decoded) - if isQuotaExhausted { - return cursorProbeResult(false, msg, resp.StatusCode, body, raw, decoded) + 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) } - // 非 200 状态码且不是额度问题 - return cursorProbeResult(false, fmt.Sprintf("HTTP %d - Token不可用", resp.StatusCode), 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) } var buf bytes.Buffer _, _ = io.Copy(&buf, io.LimitReader(resp.Body, cursorHiMaxRead)) raw := buf.Bytes() - decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding")) - isQuotaExhausted, msg := classifyCursorRawStream(decoded) - - if isQuotaExhausted { - // Token 不可用(额度用完等) - return cursorProbeResult(false, msg, resp.StatusCode, body, raw, decoded) + 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) } - - // 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) + detail := "HTTP 200;未命中内置英文关键词;" + decodeNote + ";二进制流含义与 ok 边界见 streamNote" + return cursorProbeResult(true, detail, resp.StatusCode, body, raw, decoded) } diff --git a/go/pkg/tokenprobe/probe.go b/go/pkg/tokenprobe/probe.go index 4daa625..0be8602 100644 --- a/go/pkg/tokenprobe/probe.go +++ b/go/pkg/tokenprobe/probe.go @@ -1,9 +1,8 @@ -// Package tokenprobe 使用号池内 Token 调用各厂商接口做可用性探测。 -// Cursor 走 api2.cursor.sh 的 Connect + protobuf 二进制流(非 JSON 文本接口)。 package tokenprobe import ( "bytes" + "crypto/tls" "encoding/base64" "encoding/json" "fmt" @@ -14,9 +13,13 @@ import ( "time" ) -var httpClient = &http.Client{Timeout: 25 * time.Second} +var httpClient = &http.Client{ + Timeout: 12 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, +} -// Result 探测结果(Cursor 会填充 ProbeMessage / Endpoint / BytesRead / RawPreview 等) type Result struct { OK bool `json:"ok"` Detail string `json:"detail"` @@ -30,7 +33,6 @@ type Result struct { StreamNote string `json:"streamNote,omitempty"` } -// ProbeOfficial 按号池模块探测 Token(cursor / windsurf / krio) func ProbeOfficial(module, rawToken string) Result { tok := normalizeBearerToken(strings.TrimSpace(rawToken)) if tok == "" { @@ -38,8 +40,7 @@ func ProbeOfficial(module, rawToken string) Result { } switch module { case "cursor": - // 直接使用 cursor_hi.go 中已有的完整探测函数 - return probeCursorHiAgent(tok) + return probeCursor(tok) case "windsurf": return probeWindsurf(tok) case "krio": @@ -57,12 +58,10 @@ 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{ @@ -119,13 +118,12 @@ func probeWindsurf(apiKey string) Result { } } -// probeKiro Kiro 探测 func probeKiro(accessToken string) Result { arn := findProfileArnInJWT(accessToken) if arn == "" { return Result{ OK: false, - Detail: "无法从 Token 中解析 profileArn,Kiro 暂无法自动探测(需完整登录 JWT)", + Detail: "无法从 Token 中解析 profileArn,Kiro 暂无法自动探测", } } @@ -163,7 +161,6 @@ 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, ".") @@ -181,7 +178,6 @@ 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 { @@ -190,7 +186,6 @@ func findProfileArnInJWT(raw string) string { return findProfileArnValue(m) } -// findProfileArnValue 递归查找 profileArn func findProfileArnValue(v interface{}) string { switch x := v.(type) { case map[string]interface{}: diff --git a/go/routers/api/api.go b/go/routers/api/api.go index 1ebffd7..a6788bb 100644 --- a/go/routers/api/api.go +++ b/go/routers/api/api.go @@ -11,6 +11,12 @@ func Register() { // 客户端检查更新(无需登录) beego.Router("/api/softwareupgrade/check", &controllers.ApiSoftwareUpgradeController{}, "get:Check") + // 登录器上报 Cursor 设备信息(无需登录) + beego.Router("/api/cursor/equipment/report", &controllers.ApiCursorEquipmentController{}, "post:Report") + + // 登录器使用激活码激活/续期 Cursor 设备(无需登录) + beego.Router("/api/cursor/equipment/activateByCode", &controllers.ApiCursorEquipmentController{}, "post:ActivateByCode") + // 对外提卡接口(无需登录) // GET /api/getcard?type=xianyu&module=cursor&data_type=tk beego.Router("/api/getcard", &controllers.ApiGetCardController{}, "get:GetCard") diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 28d5cf3..1cc327a 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -14,31 +14,20 @@ func Register() { // RegisterAuthRoutes 注册 backend 认证相关路由。 func RegisterAuthRoutes() { - // backend 登录相关(统一走 /backend/*) + // 登录、注册与找回密码相关 beego.Router("/backend/login", &controllers.BackendAuthController{}, "post:LoginBackend") beego.Router("/backend/sendLoginCode", &controllers.BackendAuthController{}, "post:SendLoginCode") beego.Router("/backend/loginBySms", &controllers.BackendAuthController{}, "post:LoginBySms") beego.Router("/backend/logout", &controllers.BackendAuthController{}, "post:Logout") - - // 极验与登录验证配置 - beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos") - beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos") - beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify") - - // 登录相关接口 - beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos") - beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos") - beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify") - - // 注册与找回密码 beego.Router("/backend/register", &controllers.BackendAuthController{}, "post:Register") beego.Router("/backend/sendRegisterCode", &controllers.BackendAuthController{}, "post:SendRegisterCode") beego.Router("/backend/resetPassword", &controllers.BackendAuthController{}, "post:ResetPassword") beego.Router("/backend/sendResetCode", &controllers.BackendAuthController{}, "post:SendResetCode") - // 租户站点设置 - beego.Router("/backend/normalInfos", &controllers.BackendSiteSettingsController{}, "get:GetNormalInfos") - beego.Router("/backend/saveNormalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveNormalInfos") + // 极验与登录验证配置 + beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos") + beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos") + beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify") // 菜单接口 beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu") @@ -50,6 +39,18 @@ func RegisterAuthRoutes() { beego.Router("/backend/operationLogs/:id", &controllers.BackendOperationLogController{}, "get:Detail;delete:Delete") beego.Router("/backend/operationLogs/batchDelete", &controllers.BackendOperationLogController{}, "post:BatchDelete") + // 租户站点设置 + beego.Router("/backend/normalInfos", &controllers.BackendSiteSettingsController{}, "get:GetNormalInfos") + beego.Router("/backend/saveNormalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveNormalInfos") + beego.Router("/backend/legalInfos", &controllers.BackendSiteSettingsController{}, "get:GetLegalInfos") + beego.Router("/backend/saveLegalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveLegalInfos") + beego.Router("/backend/companyInfos", &controllers.BackendSiteSettingsController{}, "get:GetCompanyInfos") + beego.Router("/backend/saveCompanyInfos", &controllers.BackendSiteSettingsController{}, "post:SaveCompanyInfos") + beego.Router("/backend/companySeo", &controllers.BackendSiteSettingsController{}, "get:GetCompanySeo") + beego.Router("/backend/saveCompanySeo", &controllers.BackendSiteSettingsController{}, "post:SaveCompanySeo") + beego.Router("/backend/loginVerifyInfos", &controllers.BackendLoginVerifyController{}, "get:GetLoginVerifyInfos") + beego.Router("/backend/saveloginVerifyInfos", &controllers.BackendLoginVerifyController{}, "post:SaveLoginVerifyInfos") + // 文件管理(yz_system_files / yz_system_files_category) beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate") beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles") @@ -101,6 +102,40 @@ func RegisterAuthRoutes() { beego.Router("/backend/erp/editPosition/:id", &controllers.BackendErpController{}, "post:EditPosition") beego.Router("/backend/erp/deletePosition/:id", &controllers.BackendErpController{}, "delete:DeletePosition") - // 文章管理相关接口 + // 文章管理 + beego.Router("/backend/articlesList", &controllers.BackendArticleController{}, "get:List") + beego.Router("/backend/allarticles", &controllers.BackendArticleController{}, "get:ListAll") + beego.Router("/backend/articles/:id", &controllers.BackendArticleController{}, "get:Detail") + beego.Router("/backend/createarticle", &controllers.BackendArticleController{}, "post:Create") + beego.Router("/backend/editarticle/:id", &controllers.BackendArticleController{}, "post:Update") + beego.Router("/backend/deletearticle/:id", &controllers.BackendArticleController{}, "delete:Delete") + beego.Router("/backend/publisharticle/:id", &controllers.BackendArticleController{}, "post:Publish") + beego.Router("/backend/unPublisharticle/:id", &controllers.BackendArticleController{}, "post:Unpublish") + beego.Router("/backend/articleRecommend/:id", &controllers.BackendArticleController{}, "post:Recommend") + beego.Router("/backend/unArticleRecommend/:id", &controllers.BackendArticleController{}, "post:Unrecommend") + beego.Router("/backend/articleTop/:id", &controllers.BackendArticleController{}, "post:Top") + beego.Router("/backend/unArticleTop/:id", &controllers.BackendArticleController{}, "post:Untop") + + beego.Router("/backend/categories", &controllers.BackendArticleCategoryController{}, "get:List") + beego.Router("/backend/allcategories", &controllers.BackendArticleCategoryController{}, "get:ListAll") + beego.Router("/backend/categories/:id", &controllers.BackendArticleCategoryController{}, "get:Detail;delete:Delete") + beego.Router("/backend/createCategory", &controllers.BackendArticleCategoryController{}, "post:Create") + beego.Router("/backend/editCategory/:id", &controllers.BackendArticleCategoryController{}, "post:Update") + beego.Router("/backend/categories/:id/status", &controllers.BackendArticleCategoryController{}, "patch:UpdateStatus") + + // 域名管理(主域名池 / 租户域名) + beego.Router("/backend/domain/pool/index", &controllers.BackendDomainPoolController{}, "get:Index") + beego.Router("/backend/domain/pool/getEnabledDomains", &controllers.BackendDomainPoolController{}, "get:GetEnabledDomains") + beego.Router("/backend/domain/pool/create", &controllers.BackendDomainPoolController{}, "post:Create") + beego.Router("/backend/domain/pool/update", &controllers.BackendDomainPoolController{}, "post:Update") + beego.Router("/backend/domain/pool/delete/:id", &controllers.BackendDomainPoolController{}, "delete:Delete") + beego.Router("/backend/domain/pool/toggleStatus", &controllers.BackendDomainPoolController{}, "post:ToggleStatus") + + beego.Router("/backend/domain/tenant/index", &controllers.BackendTenantDomainController{}, "get:Index") + beego.Router("/backend/domain/tenant/myDomains", &controllers.BackendTenantDomainController{}, "get:MyDomains") + beego.Router("/backend/domain/tenant/apply", &controllers.BackendTenantDomainController{}, "post:Apply") + beego.Router("/backend/domain/tenant/audit", &controllers.BackendTenantDomainController{}, "post:Audit") + beego.Router("/backend/domain/tenant/toggleStatus", &controllers.BackendTenantDomainController{}, "post:ToggleStatus") + beego.Router("/backend/domain/tenant/delete/:id", &controllers.BackendTenantDomainController{}, "delete:Delete") } diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go index 0c94d36..d036c98 100644 --- a/go/routers/platform/platform.go +++ b/go/routers/platform/platform.go @@ -162,6 +162,27 @@ func Register() { beego.Router("/platform/home/accountPoolDailyExtract", &controllers.PlatformHomeController{}, "get:AccountPoolDailyExtract") beego.Router("/platform/home/accountPoolInventoryTotals", &controllers.PlatformHomeController{}, "get:AccountPoolInventoryTotals") + // Cursor 设备管理(yz_platform_cursor_equipment) + beego.Router("/platform/cursor/equipment/list", &controllers.PlatformCursorEquipmentController{}, "get:List") + beego.Router("/platform/cursor/equipment/detail/:id", &controllers.PlatformCursorEquipmentController{}, "get:Detail") + beego.Router("/platform/cursor/equipment/add", &controllers.PlatformCursorEquipmentController{}, "post:Add") + beego.Router("/platform/cursor/equipment/update", &controllers.PlatformCursorEquipmentController{}, "post:Update") + beego.Router("/platform/cursor/equipment/delete/:id", &controllers.PlatformCursorEquipmentController{}, "post:Delete") + beego.Router("/platform/cursor/equipment/activate", &controllers.PlatformCursorEquipmentController{}, "post:Activate") + beego.Router("/platform/cursor/equipment/activationRecords", &controllers.PlatformCursorEquipmentController{}, "get:ActivationRecords") + beego.Router("/platform/cursor/equipment/extractRecords", &controllers.PlatformCursorEquipmentController{}, "get:ExtractRecords") + + // Cursor 激活码管理(yz_platform_cursor_activation_code) + beego.Router("/platform/cursor/activationcode/list", &controllers.PlatformCursorActivationCodeController{}, "get:List") + beego.Router("/platform/cursor/activationcode/detail/:id", &controllers.PlatformCursorActivationCodeController{}, "get:Detail") + beego.Router("/platform/cursor/activationcode/add", &controllers.PlatformCursorActivationCodeController{}, "post:Add") + beego.Router("/platform/cursor/activationcode/update", &controllers.PlatformCursorActivationCodeController{}, "post:Update") + beego.Router("/platform/cursor/activationcode/delete/:id", &controllers.PlatformCursorActivationCodeController{}, "post:Delete") + beego.Router("/platform/cursor/activationcode/generate", &controllers.PlatformCursorActivationCodeController{}, "post:Generate") + beego.Router("/platform/cursor/activationcode/enable/:id", &controllers.PlatformCursorActivationCodeController{}, "post:Enable") + beego.Router("/platform/cursor/activationcode/disable/:id", &controllers.PlatformCursorActivationCodeController{}, "post:Disable") + beego.Router("/platform/cursor/activationcode/export", &controllers.PlatformCursorActivationCodeController{}, "get:Export") + // 账号池管理(cursor/windsurf/krio) beego.Router("/platform/accountPool/cursor/list", &controllers.PlatformAccountPoolCursorController{}, "get:List") beego.Router("/platform/accountPool/cursor/add", &controllers.PlatformAccountPoolCursorController{}, "post:Add") @@ -170,6 +191,7 @@ func Register() { beego.Router("/platform/accountPool/cursor/extract", &controllers.PlatformAccountPoolCursorController{}, "post:Extract") beego.Router("/platform/accountPool/cursor/updateRemark", &controllers.PlatformAccountPoolCursorController{}, "post:UpdateRemark") beego.Router("/platform/accountPool/cursor/setUnavailable", &controllers.PlatformAccountPoolCursorController{}, "post:SetUnavailable") + beego.Router("/platform/accountPool/cursor/updateUsable", &controllers.PlatformAccountPoolCursorController{}, "post:UpdateUsable") beego.Router("/platform/accountPool/cursor/updatePlatform", &controllers.PlatformAccountPoolCursorController{}, "post:UpdatePlatform") beego.Router("/platform/accountPool/cursor/unextract", &controllers.PlatformAccountPoolCursorController{}, "post:Unextract") beego.Router("/platform/accountPool/cursor/replenish", &controllers.PlatformAccountPoolCursorController{}, "post:Replenish") diff --git a/platform/src/api/accountPool.js b/platform/src/api/accountPool.js index 719af51..bf874a3 100644 --- a/platform/src/api/accountPool.js +++ b/platform/src/api/accountPool.js @@ -59,6 +59,14 @@ export function setAccountPoolUnavailable(module, data) { }); } +export function updateAccountPoolUsable(module, data) { + return request({ + url: `${base(module)}/updateUsable`, + method: 'post', + data, + }); +} + export function updateAccountPoolPlatform(module, data) { return request({ url: `${base(module)}/updatePlatform`, diff --git a/platform/src/api/cursorActivationCode.ts b/platform/src/api/cursorActivationCode.ts new file mode 100644 index 0000000..83deb7e --- /dev/null +++ b/platform/src/api/cursorActivationCode.ts @@ -0,0 +1,106 @@ +// @ts-ignore request 封装是 JS 文件,项目未提供 TS 声明 +import request from '@/utils/request'; + +const baseUrl = '/platform/cursor/activationcode'; + +export interface CursorActivationCodeQuery { + page?: number; + pageSize?: number; + keyword?: string; + status?: number | string; + type?: number | string; + bindStatus?: number | string; +} + +export interface CursorActivationCodePayload { + id?: number | string; + code?: string; + type?: number; + status?: number; + durationDays?: number; + bindAccount?: string; + bindDeviceId?: number | string; + ownerUserId?: number | string; + ownerUserName?: string; + activatedAt?: string; + expiredAt?: string; + remark?: string; +} + +export interface GenerateActivationCodePayload { + count: number; + type?: number; + durationDays?: number; + ownerUserId?: number | string; + ownerUserName?: string; + remark?: string; +} + +export function getCursorActivationCodeList(params: CursorActivationCodeQuery) { + return request({ + url: `${baseUrl}/list`, + method: 'get', + params, + }); +} + +export function getCursorActivationCodeDetail(id: number | string) { + return request({ + url: `${baseUrl}/detail/${id}`, + method: 'get', + }); +} + +export function addCursorActivationCode(data: CursorActivationCodePayload) { + return request({ + url: `${baseUrl}/add`, + method: 'post', + data, + }); +} + +export function updateCursorActivationCode(data: CursorActivationCodePayload) { + return request({ + url: `${baseUrl}/update`, + method: 'post', + data, + }); +} + +export function deleteCursorActivationCode(id: number | string) { + return request({ + url: `${baseUrl}/delete/${id}`, + method: 'post', + }); +} + +export function generateCursorActivationCode(data: GenerateActivationCodePayload) { + return request({ + url: `${baseUrl}/generate`, + method: 'post', + data, + }); +} + +export function enableCursorActivationCode(id: number | string) { + return request({ + url: `${baseUrl}/enable/${id}`, + method: 'post', + }); +} + +export function disableCursorActivationCode(id: number | string) { + return request({ + url: `${baseUrl}/disable/${id}`, + method: 'post', + }); +} + +export function exportCursorActivationCode(params: CursorActivationCodeQuery) { + return request({ + url: `${baseUrl}/export`, + method: 'get', + params, + responseType: 'blob', + }); +} diff --git a/platform/src/api/cursorEquipment.ts b/platform/src/api/cursorEquipment.ts new file mode 100644 index 0000000..ce45f74 --- /dev/null +++ b/platform/src/api/cursorEquipment.ts @@ -0,0 +1,90 @@ +// @ts-ignore request 封装是 JS 文件,项目未提供 TS 声明 +import request from '@/utils/request'; + +const baseUrl = '/platform/cursor/equipment'; + +export interface CursorEquipmentQuery { + page?: number; + pageSize?: number; + keyword?: string; + status?: number | string; + system?: string; + os?: string; +} + +export interface CursorEquipmentPayload { + id?: number; + deviceInfo?: string; + machineCode?: string; + status?: number; + system?: string; + version?: string; + bindAccount?: string; + ownerUserId?: number; + ownerUserName?: string; + activationTime?: string; + expireTime?: string; + remark?: string; +} + +export function getCursorEquipmentList(params: CursorEquipmentQuery) { + return request({ + url: `${baseUrl}/list`, + method: 'get', + params, + }); +} + +export function getCursorEquipmentDetail(id: number | string) { + return request({ + url: `${baseUrl}/detail/${id}`, + method: 'get', + }); +} + +export function addCursorEquipment(data: CursorEquipmentPayload) { + return request({ + url: `${baseUrl}/add`, + method: 'post', + data, + }); +} + +export function updateCursorEquipment(data: CursorEquipmentPayload) { + return request({ + url: `${baseUrl}/update`, + method: 'post', + data, + }); +} + +export function deleteCursorEquipment(id: number | string) { + return request({ + url: `${baseUrl}/delete/${id}`, + method: 'post', + }); +} + +export function activateCursorEquipment(data: { id: number | string }) { + return request({ + url: `${baseUrl}/activate`, + method: 'post', + data, + }); +} + +export function getCursorEquipmentActivationRecords(params: Record) { + return request({ + url: `${baseUrl}/activationRecords`, + method: 'get', + params, + }); +} + +export function getCursorEquipmentExtractRecords(params: Record) { + return request({ + url: `${baseUrl}/extractRecords`, + method: 'get', + params, + }); +} diff --git a/platform/src/components/CommonAside.vue b/platform/src/components/CommonAside.vue index b209589..b4fef40 100644 --- a/platform/src/components/CommonAside.vue +++ b/platform/src/components/CommonAside.vue @@ -14,7 +14,6 @@ @@ -284,10 +284,29 @@ const currentModule = computed(() => { }); const displayMenus = computed(() => { - // 侧边栏始终展示完整菜单树,不随当前路由切换为“子菜单视图” + // 侧边栏始终展示完整菜单树,不随当前路由切换为"子菜单视图" return list.value; }); +const findOpenMenuPaths = (menus, targetPath, ancestors = []) => { + for (const menu of menus) { + const currentPath = menu.path || menu.id.toString(); + if (menu.path && (targetPath === menu.path || targetPath.startsWith(menu.path + "/"))) { + return [...ancestors, currentPath]; + } + if (menu.children && menu.children.length > 0) { + const found = findOpenMenuPaths(menu.children, targetPath, [...ancestors, currentPath]); + if (found) return found; + } + } + return null; +}; + +const defaultOpeneds = computed(() => { + const result = findOpenMenuPaths(displayMenus.value, route.path); + return result || []; +}); + const asideTitle = computed(() => { if (isCollapse.value) return "管理"; return "菜单"; @@ -311,7 +330,7 @@ const processMenus = (menus) => { .map((menu) => ({ id: menu.id, path: menu.path, - icon: menu.icon || "Document", + icon: menu.icon || null, title: menu.title, route: menu.path, component_path: menu.component_path, @@ -541,13 +560,13 @@ h3 { // 高亮样式 .el-menu-item.is-active { html:not(.dark) & { - background-color: rgba(57, 115, 255, 0.3) !important; + background-color: rgba(255, 255, 255, 0.2) !important; + border-left: 3px solid #ffffff; } html.dark & { background-color: rgba(60, 60, 60, 0.8) !important; } color: #ffffff !important; - border-left: 3px solid #4f84ff; margin-left: -3px; .menu-icon { @@ -574,12 +593,17 @@ h3 { } &.is-opened .el-sub-menu__title { - background: rgba(255, 255, 255, 0.08) !important; + background: rgba(255, 255, 255, 0.12) !important; + margin-left: -3px; } .el-menu-item { padding-left: 48px !important; font-size: 13px; + + &.is-active { + background: rgba(255, 255, 255, 0.18) !important; + } } } @@ -604,6 +628,10 @@ h3 { .el-sub-menu.is-opened .el-sub-menu__title { background: rgba(64, 158, 255, 0.08) !important; } + + .el-sub-menu .el-menu-item.is-active { + background: rgba(64, 158, 255, 0.15) !important; + } } } diff --git a/platform/src/views/accountpool/components/patch.vue b/platform/src/views/accountpool/components/patch.vue index 5870e0c..a6bdcd1 100644 --- a/platform/src/views/accountpool/components/patch.vue +++ b/platform/src/views/accountpool/components/patch.vue @@ -1,4 +1,3 @@ - - - - - + + + + + + + - - + + + + + + + + + + + + @@ -110,8 +141,8 @@ function statusType(status: unknown) { background layout="total, prev, pager, next, jumper" :total="total" - @update:current-page="(v) => emit('update:page', v)" - @update:page-size="(v) => emit('update:pageSize', v)" + @update:current-page="(v: number) => emit('update:page', v)" + @update:page-size="(v: number) => emit('update:pageSize', v)" /> diff --git a/platform/src/views/cursor/equipment/index.vue b/platform/src/views/cursor/equipment/index.vue index fed4f83..9225120 100644 --- a/platform/src/views/cursor/equipment/index.vue +++ b/platform/src/views/cursor/equipment/index.vue @@ -15,7 +15,7 @@ import { getCursorEquipmentExtractRecords, getCursorEquipmentList, updateCursorEquipment, -} from '@/api/cursorEquipment'; +} from '../../../api/cursorEquipment'; type EquipmentRow = Record; @@ -60,10 +60,10 @@ const extractState = reactive({ }); const statusOptions = [ - { label: '未激活', value: 'inactive' }, - { label: '正常', value: 'active' }, - { label: '禁用', value: 'disabled' }, - { label: '已过期', value: 'expired' }, + { label: '未激活', value: 0 }, + { label: '已激活', value: 1 }, + { label: '禁用', value: 3 }, + { label: '已过期', value: 2 }, ]; const osOptions = [ @@ -74,7 +74,7 @@ const osOptions = [ ]; const statusMap: Record = { - active: { label: '正常', type: 'success' }, + active: { label: '已激活', type: 'success' }, inactive: { label: '未激活', type: 'info' }, disabled: { label: '禁用', type: 'danger' }, expired: { label: '已过期', type: 'warning' }, @@ -87,7 +87,7 @@ const summary = computed(() => { const expired = tableData.value.filter((item) => item.status === 'expired').length; return [ { label: '当前页设备', value: tableData.value.length, type: 'primary' }, - { label: '正常设备', value: active, type: 'success' }, + { label: '已激活设备', value: active, type: 'success' }, { label: '未激活', value: inactive, type: 'info' }, { label: '禁用/过期', value: disabled + expired, type: 'danger' }, ]; @@ -137,24 +137,35 @@ function formatTime(value: any) { return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; } +function normalizeEquipmentStatus(status: any) { + const value = String(status ?? '').trim(); + + if (value === '0' || value === 'inactive') return 'inactive'; + if (value === '1' || value === 'active' || value === 'normal') return 'active'; + if (value === '2' || value === 'expired') return 'expired'; + if (value === '3' || value === 'disabled' || value === 'disable') return 'disabled'; + + return value || 'inactive'; +} + function normalizeRow(raw: any): EquipmentRow { - const status = String(pick(raw, 'status', 'Status') || 'inactive'); + const status = normalizeEquipmentStatus(pick(raw, 'status', 'Status')); return { id: pick(raw, 'id', 'ID', 'Id'), name: pick(raw, 'name', 'device_name', 'deviceName', 'Name', 'DeviceName'), deviceNo: pick(raw, 'device_no', 'deviceNo', 'DeviceNo', 'serial_no', 'serialNo'), machineCode: pick(raw, 'machine_code', 'machineCode', 'MachineCode', 'fingerprint'), - licenseCode: pick(raw, 'license_code', 'licenseCode', 'LicenseCode'), - os: pick(raw, 'os', 'OS', 'platform', 'Platform'), + licenseCode: pick(raw, 'bindActivationCode', 'activationCode', 'activation_code', 'code', 'Code', 'license_code', 'licenseCode', 'LicenseCode'), + os: pick(raw, 'system', 'System', 'os', 'OS', 'platform', 'Platform'), version: pick(raw, 'version', 'Version', 'client_version', 'clientVersion'), - account: pick(raw, 'account', 'Account', 'email', 'Email'), + account: pick(raw, 'bindActivationCode', 'activationCode', 'activation_code', 'code', 'Code', 'license_code', 'licenseCode', 'LicenseCode'), owner: pick(raw, 'owner', 'Owner', 'user_name', 'userName', 'tenant_name', 'tenantName'), status, activationCount: Number(pick(raw, 'activation_count', 'activationCount', 'ActivationCount') || 0), extractCount: Number(pick(raw, 'extract_count', 'extractCount', 'ExtractCount') || 0), - lastActivatedAt: formatTime(pick(raw, 'last_activated_at', 'lastActivatedAt', 'activated_at')), + lastActivatedAt: formatTime(pick(raw, 'lastActivatedAt', 'last_activated_at', 'activationTime', 'activation_time', 'activated_at', 'activatedAt')), lastExtractedAt: formatTime(pick(raw, 'last_extracted_at', 'lastExtractedAt', 'extracted_at')), - expiredAt: formatTime(pick(raw, 'expired_at', 'expiredAt', 'expire_time', 'expireTime')), + expiredAt: formatTime(pick(raw, 'expiredAt', 'expired_at', 'expireTime', 'expire_time')), createdAt: formatTime(pick(raw, 'create_time', 'created_at', 'createdAt', 'CreatedAt')), remark: pick(raw, 'remark', 'Remark'), raw, @@ -164,14 +175,23 @@ function normalizeRow(raw: any): EquipmentRow { function normalizeRecord(raw: any): EquipmentRow { return { id: pick(raw, 'id', 'ID', 'Id'), - status: pick(raw, 'status', 'Status', 'result', 'Result'), + status: pick(raw, 'status', 'Status', 'result', 'Result', 'isExtracted', 'is_extracted'), + activationCode: pick(raw, 'activationCode', 'activation_code', 'code', 'Code'), + durationDays: pick(raw, 'durationDays', 'duration_days'), + machineCode: pick(raw, 'machineCode', 'machine_code', 'MachineCode'), + deviceInfo: pick(raw, 'deviceInfo', 'device_info', 'DeviceInfo'), + expiredAt: formatTime(pick(raw, 'expiredAt', 'expired_at', 'expireTime', 'expire_time')), + activatedAt: formatTime(pick(raw, 'activatedAt', 'activated_at', 'activationTime', 'activation_time')), account: pick(raw, 'account', 'Account', 'email', 'Email'), - platform: pick(raw, 'platform', 'Platform', 'source', 'Source'), + password: pick(raw, 'password', 'Password'), + token: pick(raw, 'token', 'Token'), + platform: pick(raw, 'platform', 'Platform', 'source', 'Source', 'extractedPlatform', 'extracted_platform'), type: pick(raw, 'type', 'Type', 'data_type', 'dataType'), - content: pick(raw, 'content', 'Content', 'extract_content', 'extractContent', 'token', 'Token'), + content: pick(raw, 'content', 'Content', 'extract_content', 'extractContent'), ip: pick(raw, 'ip', 'IP', 'client_ip', 'clientIp'), clientVersion: pick(raw, 'client_version', 'clientVersion', 'version', 'Version'), createdAt: formatTime(pick(raw, 'create_time', 'created_at', 'createdAt', 'CreatedAt')), + extractedAt: formatTime(pick(raw, 'extractedAt', 'extracted_at', 'extracted_time')), remark: pick(raw, 'remark', 'Remark', 'message', 'Message'), raw, }; @@ -202,7 +222,7 @@ async function fetchList() { page: pagination.page, pageSize: pagination.pageSize, keyword: query.keyword || undefined, - status: query.status || undefined, + status: query.status === '' ? undefined : query.status, os: query.os || undefined, }); if (res?.code !== 200) { @@ -401,7 +421,7 @@ onUnmounted(() => {
@@ -450,7 +470,7 @@ onUnmounted(() => { - +