yunzerwebsiteallinone/go/controllers/api_cursor_equipment.go

721 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
// cursorIpInfo 对应 ip-api.com 返回的 JSON 结构
type cursorIpInfo struct {
Status string `json:"status"`
Country string `json:"country"`
CountryCode string `json:"countryCode"`
Region string `json:"region"`
RegionName string `json:"regionName"`
City string `json:"city"`
Zip string `json:"zip"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Timezone string `json:"timezone"`
ISP string `json:"isp"`
Org string `json:"org"`
As string `json:"as"`
Query string `json:"query"`
}
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"`
IpInfo *cursorIpInfo `json:"ipInfo"`
}
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"`
IpInfo *cursorIpInfo `json:"ipInfo"`
}
// cursorSaveIpLog 将 ipInfo 写入设备 IP 日志表(异步,失败不影响主流程)
func cursorSaveIpLog(equipmentID uint64, machineCode, source string, ip *cursorIpInfo) {
if ip == nil {
return
}
log := &models.PlatformCursorEquipmentIpLog{
EquipmentID: equipmentID,
MachineCode: machineCode,
Source: source,
Status: ip.Status,
Country: ip.Country,
CountryCode: ip.CountryCode,
Region: ip.Region,
RegionName: ip.RegionName,
City: ip.City,
Zip: ip.Zip,
Lat: ip.Lat,
Lon: ip.Lon,
Timezone: ip.Timezone,
ISP: ip.ISP,
Org: ip.Org,
AsInfo: ip.As,
Query: ip.Query,
}
_, _ = models.Orm.Insert(log)
}
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
body := c.Ctx.Input.RequestBody
if len(body) > 0 {
if err := json.Unmarshal(body, &p); err != nil {
c.jsonResult(400, "参数错误", nil)
return
}
}
// 兼容 query string 参数
if p.MachineCode == "" {
p.MachineCode = c.GetString("machineCode")
}
if p.MachineCodeSnake == "" {
p.MachineCodeSnake = c.GetString("machine_code")
}
if p.DeviceInfo == "" {
p.DeviceInfo = c.GetString("deviceInfo")
}
if p.DeviceInfoSnake == "" {
p.DeviceInfoSnake = c.GetString("device_info")
}
if p.System == "" {
p.System = c.GetString("system")
}
if p.Version == "" {
p.Version = c.GetString("version")
}
if p.BindAccount == "" {
p.BindAccount = c.GetString("bindAccount")
}
if p.BindAccountSnake == "" {
p.BindAccountSnake = c.GetString("bind_account")
}
if p.OwnerUserName == "" {
p.OwnerUserName = c.GetString("ownerUserName")
}
if p.OwnerUserNameSnake == "" {
p.OwnerUserNameSnake = c.GetString("owner_user_name")
}
if p.ActivationTime == "" {
p.ActivationTime = c.GetString("activationTime")
}
if p.ActivationTimeSnake == "" {
p.ActivationTimeSnake = c.GetString("activation_time")
}
if p.ExpireTime == "" {
p.ExpireTime = c.GetString("expireTime")
}
if p.ExpireTimeSnake == "" {
p.ExpireTimeSnake = c.GetString("expire_time")
}
if p.Remark == "" {
p.Remark = c.GetString("remark")
}
if p.Status == nil {
if s, err := c.GetInt8("status"); err == nil {
p.Status = &s
}
}
if p.OwnerUserID == nil {
if uid, err := c.GetUint64("ownerUserId"); err == nil && uid > 0 {
p.OwnerUserID = &uid
}
}
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
}
// 记录 IP 日志
cursorSaveIpLog(row.ID, machineCode, "report", p.IpInfo)
// 查询该设备最新激活码,补全激活时间和到期时间
var retActivationTime interface{} = row.ActivationTime
var retExpireTime interface{} = row.ExpireTime
var latestCode models.PlatformCursorActivationCode
cond := orm.NewCondition().
And("delete_time__isnull", true).
AndCond(orm.NewCondition().
Or("bind_device_id", row.ID).
Or("machine_code", row.MachineCode))
if err := models.Orm.QueryTable(new(models.PlatformCursorActivationCode)).
SetCond(cond).
OrderBy("-activated_at", "-id").
One(&latestCode); err == nil {
if latestCode.ActivatedAt != nil {
retActivationTime = latestCode.ActivatedAt
}
if latestCode.ExpiredAt != nil {
retExpireTime = latestCode.ExpiredAt
}
}
c.jsonResult(200, "success", map[string]interface{}{
"id": row.ID,
"machineCode": row.MachineCode,
"status": row.Status,
"created": created,
"activationTime": retActivationTime,
"expireTime": retExpireTime,
})
}
// 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
// 记录 IP 日志
cursorSaveIpLog(device.ID, machineCode, "activateByCode", p.IpInfo)
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,
})
}
type cursorHeartbeatPayload struct {
MachineCode string `json:"machineCode"`
MachineCodeSnake string `json:"machine_code"`
}
// Heartbeat POST /api/cursor/equipment/heartbeat
//
// 客户端心跳接口(无需登录),用于上报在线状态。
//
// JSON 示例:
//
// {
// "machineCode": "ABC-123"
// }
func (c *ApiCursorEquipmentController) Heartbeat() {
var p cursorHeartbeatPayload
body := c.Ctx.Input.RequestBody
if len(body) > 0 {
if err := json.Unmarshal(body, &p); err != nil {
c.jsonResult(400, "参数错误", nil)
return
}
}
machineCode := cursorFirstNonEmpty(p.MachineCode, p.MachineCodeSnake)
if machineCode == "" {
machineCode = c.GetString("machineCode")
}
if machineCode == "" {
machineCode = c.GetString("machine_code")
}
if machineCode == "" {
c.jsonResult(400, "缺少参数 machineCode/machine_code机器码", nil)
return
}
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)
if err == orm.ErrNoRows {
// 设备不存在,可能是第一次运行心跳,也可以允许在此处静默创建,或者返回 404 让客户端先进行 report
// 为了鲁棒性,如果设备未上报过,我们可以直接创建一个基础设备记录
row = models.PlatformCursorEquipment{
MachineCode: machineCode,
Status: 0, // 未激活
LastHeartbeatAt: &now,
CreateTime: now,
}
if _, insertErr := models.Orm.Insert(&row); insertErr != nil {
c.jsonResult(500, "保存设备心跳失败", nil)
return
}
} else if err != nil {
c.jsonResult(500, "设备查询失败", nil)
return
} else {
// 更新最后心跳时间
if _, updateErr := models.Orm.QueryTable(new(models.PlatformCursorEquipment)).
Filter("id", row.ID).
Update(map[string]interface{}{
"last_heartbeat_at": &now,
"update_time": now,
}); updateErr != nil {
c.jsonResult(500, "更新设备心跳失败", nil)
return
}
}
c.jsonResult(200, "success", map[string]interface{}{
"machineCode": machineCode,
"online": true,
})
}