diff --git a/controllers/api_cursor_equipment.go b/controllers/api_cursor_equipment.go index 6ce31af..d0e00e7 100644 --- a/controllers/api_cursor_equipment.go +++ b/controllers/api_cursor_equipment.go @@ -37,6 +37,25 @@ type cursorEquipmentReportPayload struct { 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 != "" { @@ -222,3 +241,263 @@ func (c *ApiCursorEquipmentController) Report() { "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/controllers/platform_cursor_equipment.go b/controllers/platform_cursor_equipment.go index 5e31799..dfdc68c 100644 --- a/controllers/platform_cursor_equipment.go +++ b/controllers/platform_cursor_equipment.go @@ -87,22 +87,86 @@ 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, - "version": row.Version, - "bindAccount": row.BindAccount, - "ownerUserId": row.OwnerUserID, - "ownerUserName": row.OwnerUserName, - "activationTime": row.ActivationTime, - "expireTime": row.ExpireTime, - "remark": row.Remark, - "createTime": row.CreateTime, - "updateTime": row.UpdateTime, + "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, } } @@ -464,11 +528,74 @@ func (c *PlatformCursorEquipmentController) ActivationRecords() { 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": []interface{}{}, - "total": 0, + "list": list, + "total": total, "page": page, "pageSize": pageSize, }) @@ -481,11 +608,73 @@ func (c *PlatformCursorEquipmentController) ExtractRecords() { 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": []interface{}{}, - "total": 0, + "list": list, + "total": total, "page": page, "pageSize": pageSize, }) diff --git a/routers/api/api.go b/routers/api/api.go index 82bfe73..a6788bb 100644 --- a/routers/api/api.go +++ b/routers/api/api.go @@ -14,6 +14,9 @@ func Register() { // 登录器上报 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")