Compare commits
30
Commits
01426eda44
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a57f1cf14 | ||
|
|
6788d49f47 | ||
|
|
6b8651af25 | ||
|
|
df02280086 | ||
|
|
269fbd08ff | ||
|
|
e6b84aad80 | ||
|
|
31fc86e878 | ||
|
|
6d9977cb76 | ||
|
|
e685c9c0c7 | ||
|
|
6de78a5a2a | ||
|
|
ed34505ea9 | ||
|
|
283a2b7a80 | ||
|
|
2a60d34711 | ||
|
|
824199c87c | ||
|
|
61c151f62a | ||
|
|
fd699b2821 | ||
|
|
0aed67fb95 | ||
|
|
d9df691298 | ||
|
|
53decd0084 | ||
|
|
ccad4c05f7 | ||
|
|
fa72952f15 | ||
|
|
ea0e84c93c | ||
|
|
f4403701c1 | ||
|
|
d9f0abfc10 | ||
|
|
5596f9da22 | ||
|
|
3d1a1c9711 | ||
|
|
f84df652d9 | ||
|
|
6df84b0584 | ||
|
|
4a4bd8f67c | ||
|
|
36d2a8945e |
@@ -2,6 +2,18 @@ appname = server
|
||||
httpport = 8081
|
||||
runmode = dev
|
||||
|
||||
# 启用请求体复制(允许多次读取请求体)
|
||||
copyrequestbody = true
|
||||
|
||||
# 服务器超时配置(支持大文件上传)
|
||||
# 0 表示不设置超时限制
|
||||
ServerTimeOut = 0
|
||||
# 最大请求体大小(字节),0 表示不限制
|
||||
MaxMemory = 0
|
||||
|
||||
# 最大请求体大小(用于普通请求,10MB)
|
||||
maxmemory = 10485760
|
||||
|
||||
# 数据库配置
|
||||
# MySQL - 远程连接配置
|
||||
mysqluser = go-platform
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// ApiGetCardController 对外提卡接口(无需登录)
|
||||
// GET /api/getcard?type=xianyu&module=cursor
|
||||
type ApiGetCardController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// validPlatformTypes 支持的来源平台
|
||||
var validPlatformTypes = map[string]bool{
|
||||
"xianyu": true,
|
||||
"pinduoduo": true,
|
||||
"jingdong": true,
|
||||
"douyin": true,
|
||||
"local": true,
|
||||
}
|
||||
|
||||
// validModules 支持的号池模块
|
||||
var validModules = map[string]bool{
|
||||
"cursor": true,
|
||||
"windsurf": true,
|
||||
"krio": true,
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) cardErr(_ int, _ int, msg string) {
|
||||
c.Ctx.Output.SetStatus(200)
|
||||
c.Ctx.Output.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
_ = c.Ctx.Output.Body([]byte("error:" + msg))
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) cardOK(text string) {
|
||||
c.Ctx.Output.SetStatus(200)
|
||||
c.Ctx.Output.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
_ = c.Ctx.Output.Body([]byte(text))
|
||||
}
|
||||
|
||||
// GetCard 提取一张卡(不可重复提取)
|
||||
// GET /api/getcard?type=xianyu&module=cursor&data_type=tk
|
||||
//
|
||||
// 参数:
|
||||
// - type (必填) 来源平台:xianyu / taobao / pinduoduo / jingdong / local
|
||||
// - module (必填) 号池模块:cursor / windsurf / krio
|
||||
// - data_type (可选) 账号类型:account / tk / account_tk,不传则取任意未提取的
|
||||
func (c *ApiGetCardController) GetCard() {
|
||||
platform := c.GetString("type")
|
||||
module := c.GetString("module")
|
||||
dataType := c.GetString("data_type")
|
||||
|
||||
// 参数校验
|
||||
if platform == "" {
|
||||
c.cardErr(400, 400, "缺少参数 type(来源平台)")
|
||||
return
|
||||
}
|
||||
if !validPlatformTypes[platform] {
|
||||
c.cardErr(400, 400, fmt.Sprintf("不支持的平台类型: %s,支持: xianyu/taobao/pinduoduo/jingdong/local", platform))
|
||||
return
|
||||
}
|
||||
if module == "" {
|
||||
c.cardErr(400, 400, "缺少参数 module(号池模块)")
|
||||
return
|
||||
}
|
||||
if !validModules[module] {
|
||||
c.cardErr(400, 400, fmt.Sprintf("不支持的模块: %s,支持: cursor/windsurf/krio", module))
|
||||
return
|
||||
}
|
||||
if dataType != "" && !isValidPoolType(dataType) {
|
||||
c.cardErr(400, 400, "data_type 不合法,支持: account/tk/account_tk")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
switch module {
|
||||
case "cursor":
|
||||
c.extractCursor(platform, dataType, now)
|
||||
case "windsurf":
|
||||
c.extractWindsurf(platform, dataType, now)
|
||||
case "krio":
|
||||
c.extractKrio(platform, dataType, now)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractCursor(platform, dataType string, now time.Time) {
|
||||
c.extractWithProbe("cursor", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolCursor
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
|
||||
Filter("is_extracted", 0).
|
||||
Filter("delete_time__isnull", true)
|
||||
if dataType != "" {
|
||||
qs = qs.Filter("data_type", dataType)
|
||||
}
|
||||
if err := qs.OrderBy("id").One(&row); err != nil {
|
||||
return 0, nil, nil, "", "", nil, err
|
||||
}
|
||||
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, row.IsUsed, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractWindsurf(platform, dataType string, now time.Time) {
|
||||
c.extractWithProbe("windsurf", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolWindsurf
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
|
||||
Filter("is_extracted", 0).
|
||||
Filter("delete_time__isnull", true)
|
||||
if dataType != "" {
|
||||
qs = qs.Filter("data_type", dataType)
|
||||
}
|
||||
if err := qs.OrderBy("id").One(&row); err != nil {
|
||||
return 0, nil, nil, "", "", nil, err
|
||||
}
|
||||
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ApiGetCardController) extractKrio(platform, dataType string, now time.Time) {
|
||||
c.extractWithProbe("krio", platform, dataType, now, func() (uint64, *string, *string, string, string, *int8, error) {
|
||||
var row models.PlatformAccountPoolKiro
|
||||
qs := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
|
||||
Filter("is_extracted", 0).
|
||||
Filter("delete_time__isnull", true)
|
||||
if dataType != "" {
|
||||
qs = qs.Filter("data_type", dataType)
|
||||
}
|
||||
if err := qs.OrderBy("id").One(&row); err != nil {
|
||||
return 0, nil, nil, "", "", nil, err
|
||||
}
|
||||
return row.ID, &row.Account, &row.Password, row.Token, row.DataType, nil, nil
|
||||
})
|
||||
}
|
||||
|
||||
type poolRowFetcher func() (id uint64, account, password *string, token, rowDataType string, isUsed *int8, err error)
|
||||
|
||||
// extractWithProbe 按 id 顺序提取并探测 Token 可用性;不可用则标记已提取并继续下一条。
|
||||
func (c *ApiGetCardController) extractWithProbe(
|
||||
module, platform, dataType string,
|
||||
now time.Time,
|
||||
fetch poolRowFetcher,
|
||||
) {
|
||||
for {
|
||||
id, account, password, token, rowDataType, isUsed, err := fetch()
|
||||
if err != nil {
|
||||
if err == orm.ErrNoRows {
|
||||
c.cardErr(404, 404, "暂无可用卡密")
|
||||
} else {
|
||||
c.cardErr(500, 500, "查询失败")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
tableName := poolTableName(module)
|
||||
if tableName == "" {
|
||||
c.cardErr(500, 500, "无效模块")
|
||||
return
|
||||
}
|
||||
_, err = models.Orm.QueryTable(tableName).
|
||||
Filter("id", id).
|
||||
Update(map[string]interface{}{
|
||||
"is_extracted": 1,
|
||||
"extracted_time": now,
|
||||
"extracted_platform": platform,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
c.cardErr(500, 500, "提取失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 已有探测结论:可用则直接返回,不可用则继续下一条。
|
||||
if known, available := poolIsUsedAvailable(isUsed); known {
|
||||
if available {
|
||||
c.cardOK(buildCardResult(account, password, token, rowDataType))
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !poolProbeToken(module, rowDataType, token, id) {
|
||||
continue
|
||||
}
|
||||
|
||||
c.cardOK(buildCardResult(account, password, token, rowDataType))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// buildCardResult 根据账号类型返回格式化字符串
|
||||
func buildCardResult(account, password *string, token string, dataType string) string {
|
||||
acc := ""
|
||||
pwd := ""
|
||||
if account != nil {
|
||||
acc = *account
|
||||
}
|
||||
if password != nil {
|
||||
pwd = *password
|
||||
}
|
||||
switch dataType {
|
||||
case "account":
|
||||
return fmt.Sprintf("账号:%s / 密码:%s", acc, pwd)
|
||||
case "account_tk":
|
||||
return fmt.Sprintf("账号:%s / 密码:%s / Token:%s", acc, pwd, token)
|
||||
default: // tk
|
||||
return token
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/passwordutil"
|
||||
"server/services"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type BackendAdminUserController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type backendUserInfoDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
Tid uint64 `json:"tid"`
|
||||
Uid uint64 `json:"uid"`
|
||||
Account *string `json:"account"`
|
||||
Name *string `json:"name"`
|
||||
Phone *string `json:"phone"`
|
||||
Email *string `json:"email"`
|
||||
Sex uint8 `json:"sex"`
|
||||
Birth *string `json:"birth"`
|
||||
IsDefault int8 `json:"is_default"`
|
||||
Status int8 `json:"status"`
|
||||
Remark *string `json:"remark"`
|
||||
CreateTime string `json:"create_time"`
|
||||
UpdateTime *string `json:"update_time"`
|
||||
TenantName string `json:"tenant_name"`
|
||||
TenantCode string `json:"tenant_code"`
|
||||
}
|
||||
|
||||
type backendTenantUserPayload struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
Uid uint64 `json:"uid"`
|
||||
Account *string `json:"account"`
|
||||
Name *string `json:"name"`
|
||||
Phone *string `json:"phone"`
|
||||
Email *string `json:"email"`
|
||||
Sex *uint8 `json:"sex"`
|
||||
Birth *string `json:"birth"`
|
||||
Password *string `json:"password"`
|
||||
IsDefault *int8 `json:"is_default"`
|
||||
Status *int8 `json:"status"`
|
||||
Remark *string `json:"remark"`
|
||||
}
|
||||
|
||||
type backendChangePasswordPayload struct {
|
||||
ID uint64 `json:"id"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func formatBackendBirth(birth *string) *string {
|
||||
if birth == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := strings.TrimSpace(*birth)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(s) >= 10 {
|
||||
date := s[:10]
|
||||
return &date
|
||||
}
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
func toBackendUserInfoDTO(u models.SystemTenantUser) backendUserInfoDTO {
|
||||
var updateTime *string
|
||||
if u.UpdateTime != nil {
|
||||
s := u.UpdateTime.Format("2006-01-02 15:04:05")
|
||||
updateTime = &s
|
||||
}
|
||||
|
||||
tenantName := "未知租户"
|
||||
tenantCode := ""
|
||||
|
||||
tenant, err := services.GetTenantByID(u.Tid)
|
||||
if err == nil && tenant != nil {
|
||||
tenantName = tenant.TenantName
|
||||
tenantCode = tenant.TenantCode
|
||||
}
|
||||
|
||||
return backendUserInfoDTO{
|
||||
ID: u.ID,
|
||||
Tid: u.Tid,
|
||||
Uid: u.Uid,
|
||||
Account: u.Account,
|
||||
Name: u.Name,
|
||||
Phone: u.Phone,
|
||||
Email: u.Email,
|
||||
Sex: u.Sex,
|
||||
Birth: formatBackendBirth(u.Birth),
|
||||
IsDefault: u.IsDefault,
|
||||
Status: u.Status,
|
||||
Remark: u.Remark,
|
||||
CreateTime: u.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
UpdateTime: updateTime,
|
||||
TenantName: tenantName,
|
||||
TenantCode: tenantCode,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BackendAdminUserController) getJWTUidTid() (uint64, uint64) {
|
||||
var uid uint64
|
||||
var tid uint64
|
||||
|
||||
data := c.Ctx.Input.Data()
|
||||
|
||||
if jwtUid := data["uid"]; jwtUid != nil {
|
||||
if v, ok := jwtUid.(uint64); ok {
|
||||
uid = v
|
||||
}
|
||||
}
|
||||
|
||||
if jwtTid := data["tid"]; jwtTid != nil {
|
||||
if v, ok := jwtTid.(uint64); ok {
|
||||
tid = v
|
||||
}
|
||||
}
|
||||
|
||||
return uid, tid
|
||||
}
|
||||
|
||||
func (c *BackendAdminUserController) parseTenantUserPayload() (backendTenantUserPayload, bool) {
|
||||
var p backendTenantUserPayload
|
||||
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||||
_ = c.ServeJSON()
|
||||
return backendTenantUserPayload{}, false
|
||||
}
|
||||
|
||||
return p, true
|
||||
}
|
||||
|
||||
func findBackendTenantUser(idOrUid uint64, jwtTid uint64) (*models.SystemTenantUser, error) {
|
||||
var row models.SystemTenantUser
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("delete_time__isnull", true)
|
||||
|
||||
if jwtTid > 0 {
|
||||
qs = qs.Filter("tid", jwtTid)
|
||||
}
|
||||
|
||||
err := qs.Filter("id", idOrUid).One(&row)
|
||||
if err == nil {
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
qs = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("delete_time__isnull", true)
|
||||
|
||||
if jwtTid > 0 {
|
||||
qs = qs.Filter("tid", jwtTid)
|
||||
}
|
||||
|
||||
err = qs.Filter("uid", idOrUid).One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// GetAllUsers 获取当前租户后台用户列表
|
||||
// GET /backend/getAllUsers
|
||||
func (c *BackendAdminUserController) GetAllUsers() {
|
||||
_, jwtTid := c.getJWTUidTid()
|
||||
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
tid, _ := c.GetUint64("tid")
|
||||
|
||||
if jwtTid > 0 {
|
||||
tid = jwtTid
|
||||
}
|
||||
|
||||
cond := orm.NewCondition().And("delete_time__isnull", true)
|
||||
|
||||
if tid > 0 {
|
||||
cond = cond.And("tid", tid)
|
||||
}
|
||||
|
||||
if keyword != "" {
|
||||
kwCond := orm.NewCondition().
|
||||
Or("name__icontains", keyword).
|
||||
Or("phone__icontains", keyword).
|
||||
Or("email__icontains", keyword).
|
||||
Or("account__icontains", keyword)
|
||||
cond = cond.AndCond(kwCond)
|
||||
}
|
||||
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
SetCond(cond).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]backendUserInfoDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, toBackendUserInfoDTO(row))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": len(list),
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantUsers 获取指定租户后台用户
|
||||
// GET /backend/getTenantUsers/:tid
|
||||
func (c *BackendAdminUserController) GetTenantUsers() {
|
||||
tidStr := c.Ctx.Input.Param(":tid")
|
||||
tid, _ := strconv.ParseUint(tidStr, 10, 64)
|
||||
|
||||
_, jwtTid := c.getJWTUidTid()
|
||||
if jwtTid > 0 {
|
||||
tid = jwtTid
|
||||
}
|
||||
|
||||
if tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tid 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]backendUserInfoDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
list = append(list, toBackendUserInfoDTO(row))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": len(list),
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetUserInfo 获取后台租户用户详情
|
||||
// GET /backend/getUserInfo/:id
|
||||
func (c *BackendAdminUserController) GetUserInfo() {
|
||||
jwtUid, jwtTid := c.getJWTUidTid()
|
||||
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, _ := strconv.ParseUint(idStr, 10, 64)
|
||||
|
||||
if id == 0 {
|
||||
id = jwtUid
|
||||
}
|
||||
|
||||
if id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "未登录或非法请求"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
u, err := findBackendTenantUser(id, jwtTid)
|
||||
if err != nil || u == nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "后台用户信息不存在"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": toBackendUserInfoDTO(*u),
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddUser 添加后台租户用户
|
||||
// POST /backend/addUser
|
||||
func (c *BackendAdminUserController) AddUser() {
|
||||
p, ok := c.parseTenantUserPayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
_, jwtTid := c.getJWTUidTid()
|
||||
if jwtTid > 0 {
|
||||
p.Tid = jwtTid
|
||||
}
|
||||
|
||||
if p.Tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tid 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if p.Account == nil || strings.TrimSpace(*p.Account) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "account 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if p.Password == nil || strings.TrimSpace(*p.Password) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "password 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
account := strings.TrimSpace(*p.Account)
|
||||
p.Account = &account
|
||||
|
||||
hashed, err := passwordutil.Hash(strings.TrimSpace(*p.Password))
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
p.Password = &hashed
|
||||
|
||||
if p.Uid == 0 {
|
||||
uid, err := generateTenantUID(p.Tid)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "生成用户ID失败"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
p.Uid = uid
|
||||
}
|
||||
|
||||
isDefault := int8(0)
|
||||
if p.IsDefault != nil {
|
||||
isDefault = *p.IsDefault
|
||||
}
|
||||
|
||||
status := int8(1)
|
||||
if p.Status != nil {
|
||||
status = *p.Status
|
||||
}
|
||||
|
||||
id, err := services.BindTenantUser(
|
||||
p.Tid,
|
||||
p.Uid,
|
||||
p.Account,
|
||||
p.Name,
|
||||
p.Phone,
|
||||
p.Email,
|
||||
p.Sex,
|
||||
p.Birth,
|
||||
p.Password,
|
||||
isDefault,
|
||||
status,
|
||||
p.Remark,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "添加失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if isDefault == 1 {
|
||||
_ = services.SetDefaultTenant(p.Uid, p.Tid)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"id": id},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// EditUser 编辑后台租户用户
|
||||
// POST /backend/editUser/:id
|
||||
func (c *BackendAdminUserController) EditUser() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, _ := strconv.ParseUint(idStr, 10, 64)
|
||||
|
||||
if id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
p, ok := c.parseTenantUserPayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
_, jwtTid := c.getJWTUidTid()
|
||||
|
||||
row, err := findBackendTenantUser(id, jwtTid)
|
||||
if err != nil || row == nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "用户不存在"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
update := map[string]interface{}{}
|
||||
|
||||
if p.Tid > 0 && jwtTid == 0 {
|
||||
update["tid"] = p.Tid
|
||||
}
|
||||
|
||||
if p.Uid > 0 {
|
||||
update["uid"] = p.Uid
|
||||
}
|
||||
|
||||
if p.Account != nil {
|
||||
account := strings.TrimSpace(*p.Account)
|
||||
if account != "" {
|
||||
update["account"] = account
|
||||
}
|
||||
}
|
||||
|
||||
if p.Name != nil {
|
||||
update["name"] = *p.Name
|
||||
}
|
||||
|
||||
if p.Phone != nil {
|
||||
update["phone"] = *p.Phone
|
||||
}
|
||||
|
||||
if p.Email != nil {
|
||||
update["email"] = *p.Email
|
||||
}
|
||||
|
||||
if p.Sex != nil {
|
||||
update["sex"] = *p.Sex
|
||||
}
|
||||
|
||||
if p.Birth != nil {
|
||||
birth := strings.TrimSpace(*p.Birth)
|
||||
if birth == "" {
|
||||
update["birth"] = nil
|
||||
} else {
|
||||
update["birth"] = birth
|
||||
}
|
||||
}
|
||||
|
||||
if p.Password != nil && strings.TrimSpace(*p.Password) != "" {
|
||||
hashed, err := passwordutil.Hash(strings.TrimSpace(*p.Password))
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
update["password"] = hashed
|
||||
}
|
||||
|
||||
if p.IsDefault != nil {
|
||||
update["is_default"] = *p.IsDefault
|
||||
}
|
||||
|
||||
if p.Status != nil {
|
||||
update["status"] = *p.Status
|
||||
}
|
||||
|
||||
if p.Remark != nil {
|
||||
update["remark"] = *p.Remark
|
||||
}
|
||||
|
||||
if len(update) == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无更新字段"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("id", row.ID).
|
||||
Update(update)
|
||||
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "编辑失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if p.IsDefault != nil && *p.IsDefault == 1 {
|
||||
_ = services.SetDefaultTenant(row.Uid, row.Tid)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteUser 删除后台租户用户
|
||||
// DELETE /backend/deleteUser/:id
|
||||
func (c *BackendAdminUserController) DeleteUser() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, _ := strconv.ParseUint(idStr, 10, 64)
|
||||
|
||||
if id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
_, jwtTid := c.getJWTUidTid()
|
||||
|
||||
row, err := findBackendTenantUser(id, jwtTid)
|
||||
if err != nil || row == nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "用户不存在"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.UnbindTenantUser(row.ID); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ChangePassword 修改后台租户用户密码
|
||||
// POST /backend/changePassword
|
||||
func (c *BackendAdminUserController) ChangePassword() {
|
||||
var p backendChangePasswordPayload
|
||||
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if p.ID == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(p.Password) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "password 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
_, jwtTid := c.getJWTUidTid()
|
||||
|
||||
row, err := findBackendTenantUser(p.ID, jwtTid)
|
||||
if err != nil || row == nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "用户不存在"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
hashed, err := passwordutil.Hash(strings.TrimSpace(p.Password))
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("id", row.ID).
|
||||
Update(map[string]interface{}{
|
||||
"password": hashed,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "修改失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "修改成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,954 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendArticleController CMS 文章管理
|
||||
type BackendArticleController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// BackendArticleCategoryController CMS 文章分类管理
|
||||
type BackendArticleCategoryController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendArticleController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func (c *BackendArticleCategoryController) cmsClaims() (*jwtutil.Claims, error) {
|
||||
return cmsBackendClaims(&c.Controller)
|
||||
}
|
||||
|
||||
func cmsBackendClaims(c *beego.Controller) (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, fmt.Errorf("认证信息格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的token")
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func cmsEffectiveTid(c *beego.Controller, claims *jwtutil.Claims) uint64 {
|
||||
_ = c.ParseForm(1 << 20)
|
||||
if tid, err := c.GetUint64("tid"); err == nil && tid > 0 {
|
||||
return tid
|
||||
}
|
||||
if h := strings.TrimSpace(c.Ctx.Request.Header.Get("X-Tenant-Id")); h != "" {
|
||||
if v, e := strconv.ParseUint(h, 10, 64); e == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
if claims != nil && claims.TenantId > 0 {
|
||||
return uint64(claims.TenantId)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *BackendArticleController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendArticleCategoryController) cmsJSONErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func cmsEnsureTables(c *beego.Controller) bool {
|
||||
if err := models.EnsureCmsArticleTables(); err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "初始化文章表失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func cmsParseUintArg(v interface{}) uint64 {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
if x > 0 {
|
||||
return uint64(x)
|
||||
}
|
||||
case string:
|
||||
if n, err := strconv.ParseUint(strings.TrimSpace(x), 10, 64); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func cmsArticleToListItem(row models.CmsArticle, cateName string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"title": row.Title,
|
||||
"author": row.Author,
|
||||
"cate": cateName,
|
||||
"cate_id": row.CateID,
|
||||
"status": row.Status,
|
||||
"views": row.Views,
|
||||
"likes": row.Likes,
|
||||
"top": row.Top,
|
||||
"recommend": row.Recommend,
|
||||
"publish_date": models.CmsFormatTime(row.PublishTime),
|
||||
"update_time": models.CmsFormatTime(row.UpdateTime),
|
||||
}
|
||||
}
|
||||
|
||||
func cmsArticleToDetail(row models.CmsArticle, cateName string) map[string]interface{} {
|
||||
pub := models.CmsFormatTime(row.PublishTime)
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"title": row.Title,
|
||||
"author": row.Author,
|
||||
"cate": cateName,
|
||||
"cate_id": row.CateID,
|
||||
"content": row.Content,
|
||||
"desc": row.Desc,
|
||||
"image": row.Image,
|
||||
"is_trans": row.IsTrans,
|
||||
"transurl": row.TransURL,
|
||||
"status": row.Status,
|
||||
"views": row.Views,
|
||||
"view_count": row.Views,
|
||||
"likes": row.Likes,
|
||||
"top": row.Top,
|
||||
"recommend": row.Recommend,
|
||||
"publish_time": pub,
|
||||
"publish_date": pub,
|
||||
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
"update_time": models.CmsFormatTime(row.UpdateTime),
|
||||
}
|
||||
}
|
||||
|
||||
func cmsCategoryToMap(row models.CmsArticleCategory) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"name": row.Name,
|
||||
"label": row.Name,
|
||||
"cid": row.Cid,
|
||||
"parentId": row.Cid,
|
||||
"image": row.Image,
|
||||
"desc": row.Desc,
|
||||
"remark": row.Desc,
|
||||
"sort": row.Sort,
|
||||
"status": row.Status,
|
||||
}
|
||||
}
|
||||
|
||||
// List GET /backend/articlesList
|
||||
func (c *BackendArticleController) List() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
if pageSize > 200 {
|
||||
pageSize = 200
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
cateFilter := strings.TrimSpace(c.GetString("cate"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("title__icontains", keyword)
|
||||
}
|
||||
if cateFilter != "" {
|
||||
if cid, err := strconv.ParseUint(cateFilter, 10, 64); err == nil && cid > 0 {
|
||||
qs = qs.Filter("cate_id", cid)
|
||||
}
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
var rows []models.CmsArticle
|
||||
offset := (page - 1) * pageSize
|
||||
_, err = qs.OrderBy("-top", "-id").Limit(pageSize, offset).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取文章列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
cateIDs := make([]uint64, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
if r.CateID > 0 {
|
||||
cateIDs = append(cateIDs, r.CateID)
|
||||
}
|
||||
}
|
||||
cateNames := models.CmsCategoryNameMap(tid, cateIDs)
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsArticleToListItem(r, cateNames[r.CateID]))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": total},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ListAll GET /backend/allarticles
|
||||
func (c *BackendArticleController) ListAll() {
|
||||
c.List()
|
||||
}
|
||||
|
||||
// Detail GET /backend/articles/:id
|
||||
func (c *BackendArticleController) Detail() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.CmsArticle
|
||||
err = models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
c.cmsJSONErr(404, 404, "文章不存在")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
cateName := ""
|
||||
if row.CateID > 0 {
|
||||
names := models.CmsCategoryNameMap(tid, []uint64{row.CateID})
|
||||
cateName = names[row.CateID]
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": cmsArticleToDetail(row, cateName),
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type cmsArticlePayload struct {
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Cate interface{} `json:"cate"`
|
||||
Content string `json:"content"`
|
||||
Desc string `json:"desc"`
|
||||
Image string `json:"image"`
|
||||
IsTrans int8 `json:"is_trans"`
|
||||
TransURL *string `json:"transurl"`
|
||||
Status int8 `json:"status"`
|
||||
IgnoreSimilarity int `json:"ignore_similarity"`
|
||||
}
|
||||
|
||||
// Create POST /backend/createarticle
|
||||
func (c *BackendArticleController) Create() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsArticlePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
c.cmsJSONErr(400, 400, "标题不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if p.IgnoreSimilarity != 1 {
|
||||
similar, serr := models.CmsSimilarArticles(tid, title, 5)
|
||||
if serr == nil && len(similar) > 0 {
|
||||
c.Ctx.Output.SetStatus(409)
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 409,
|
||||
"msg": "检测到相似标题",
|
||||
"data": map[string]interface{}{"similar_articles": similar},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
cateID := cmsParseUintArg(p.Cate)
|
||||
row := models.CmsArticle{
|
||||
Tid: tid,
|
||||
Title: title,
|
||||
Author: strings.TrimSpace(p.Author),
|
||||
CateID: cateID,
|
||||
Content: p.Content,
|
||||
Desc: strings.TrimSpace(p.Desc),
|
||||
Image: strings.TrimSpace(p.Image),
|
||||
IsTrans: p.IsTrans,
|
||||
TransURL: p.TransURL,
|
||||
Status: p.Status,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update POST /backend/editarticle/:id
|
||||
func (c *BackendArticleController) Update() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsArticlePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
fields := map[string]interface{}{
|
||||
"title": strings.TrimSpace(p.Title),
|
||||
"author": strings.TrimSpace(p.Author),
|
||||
"cate_id": cmsParseUintArg(p.Cate),
|
||||
"content": p.Content,
|
||||
"desc": strings.TrimSpace(p.Desc),
|
||||
"image": strings.TrimSpace(p.Image),
|
||||
"is_trans": p.IsTrans,
|
||||
"transurl": p.TransURL,
|
||||
"status": p.Status,
|
||||
"update_time": now,
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(fields)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "文章不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/deletearticle/:id
|
||||
func (c *BackendArticleController) Delete() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "文章不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendArticleController) setArticleFlag(field string, value int8) {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
fields := map[string]interface{}{field: value, "update_time": now}
|
||||
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(fields)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "操作失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "文章不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendArticleController) Publish() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var uid uint64
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if len(raw) > 0 {
|
||||
var body struct {
|
||||
UID uint64 `json:"uid"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &body)
|
||||
uid = body.UID
|
||||
}
|
||||
if uid == 0 && claims != nil {
|
||||
uid = uint64(claims.UserID)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
fields := map[string]interface{}{
|
||||
"status": int8(2),
|
||||
"publish_time": now,
|
||||
"publisher_id": uid,
|
||||
"update_time": now,
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(fields)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "发布失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "文章不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "发布成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendArticleController) Unpublish() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"status": int8(3), "update_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "下架失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "文章不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "下架成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendArticleController) Recommend() { c.setArticleFlag("recommend", 1) }
|
||||
func (c *BackendArticleController) Unrecommend() { c.setArticleFlag("recommend", 0) }
|
||||
func (c *BackendArticleController) Top() { c.setArticleFlag("top", 1) }
|
||||
func (c *BackendArticleController) Untop() { c.setArticleFlag("top", 0) }
|
||||
|
||||
// List GET /backend/categories
|
||||
func (c *BackendArticleCategoryController) List() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 0)
|
||||
if pageSize == 0 {
|
||||
pageSize, _ = c.GetInt("limit", 1000)
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 1000
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
qs := models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("name__icontains", keyword)
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
var rows []models.CmsArticleCategory
|
||||
offset := (page - 1) * pageSize
|
||||
_, err = qs.OrderBy("sort", "id").Limit(pageSize, offset).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取分类失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsCategoryToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": total, "records": list},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ListAll GET /backend/allcategories
|
||||
func (c *BackendArticleCategoryController) ListAll() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
qs := models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("name__icontains", keyword)
|
||||
}
|
||||
|
||||
var rows []models.CmsArticleCategory
|
||||
_, err = qs.OrderBy("sort", "id").All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
c.cmsJSONErr(500, 500, "获取分类失败")
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, cmsCategoryToMap(r))
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Detail GET /backend/categories/:id
|
||||
func (c *BackendArticleCategoryController) Detail() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.CmsArticleCategory
|
||||
err = models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
c.cmsJSONErr(404, 404, "分类不存在")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": cmsCategoryToMap(row)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type cmsCategoryPayload struct {
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Desc string `json:"desc"`
|
||||
Sort int `json:"sort"`
|
||||
Status int8 `json:"status"`
|
||||
Cid uint64 `json:"cid"`
|
||||
}
|
||||
|
||||
// Create POST /backend/createCategory
|
||||
func (c *BackendArticleCategoryController) Create() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsCategoryPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(p.Name)
|
||||
if name == "" {
|
||||
c.cmsJSONErr(400, 400, "分类名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
row := models.CmsArticleCategory{
|
||||
Tid: tid,
|
||||
Cid: p.Cid,
|
||||
Name: name,
|
||||
Image: strings.TrimSpace(p.Image),
|
||||
Desc: strings.TrimSpace(p.Desc),
|
||||
Sort: p.Sort,
|
||||
Status: p.Status,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "创建失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update POST /backend/editCategory/:id
|
||||
func (c *BackendArticleCategoryController) Update() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p cmsCategoryPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{
|
||||
"name": strings.TrimSpace(p.Name),
|
||||
"image": strings.TrimSpace(p.Image),
|
||||
"desc": strings.TrimSpace(p.Desc),
|
||||
"sort": p.Sort,
|
||||
"status": p.Status,
|
||||
"cid": p.Cid,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/categories/:id
|
||||
func (c *BackendArticleCategoryController) Delete() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
childCnt, _ := models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||
Filter("tid", tid).
|
||||
Filter("cid", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if childCnt > 0 {
|
||||
c.cmsJSONErr(400, 400, "请先删除子分类")
|
||||
return
|
||||
}
|
||||
|
||||
articleCnt, _ := models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("tid", tid).
|
||||
Filter("cate_id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if articleCnt > 0 {
|
||||
c.cmsJSONErr(400, 400, "该分类下还有文章,无法删除")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "删除失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateStatus PATCH /backend/categories/:id/status
|
||||
func (c *BackendArticleCategoryController) UpdateStatus() {
|
||||
if !cmsEnsureTables(&c.Controller) {
|
||||
return
|
||||
}
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
id, _ := c.GetUint64(":id")
|
||||
if id == 0 {
|
||||
c.cmsJSONErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.cmsJSONErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"status": p.Status, "update_time": now})
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "更新失败")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.cmsJSONErr(404, 404, "分类不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type backendAuthLoginRequest struct {
|
||||
TenantName string `json:"tenant_name"`
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
// 极验4验证参数
|
||||
CaptchaID string `json:"captcha_id"`
|
||||
LotNumber string `json:"lot_number"`
|
||||
PassToken string `json:"pass_token"`
|
||||
GenTime string `json:"gen_time"`
|
||||
CaptchaOutput string `json:"captcha_output"`
|
||||
}
|
||||
|
||||
// BackendAuthController backend 端认证控制器
|
||||
type BackendAuthController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendAuthController) serveJSON(data map[string]interface{}) {
|
||||
c.Data["json"] = data
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// LoginBackend backend 登录(需要租户)
|
||||
func (c *BackendAuthController) LoginBackend() {
|
||||
var req backendAuthLoginRequest
|
||||
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(body) == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
req.TenantName = strings.TrimSpace(req.TenantName)
|
||||
req.Account = strings.TrimSpace(req.Account)
|
||||
req.Password = strings.TrimSpace(req.Password)
|
||||
if req.TenantName == "" || req.Account == "" || req.Password == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "租户名称、用户名或密码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
if cfg.OpenVerifyEnabled == 1 {
|
||||
if cfg.VerifyType == "geetest4" {
|
||||
if req.LotNumber == "" || req.PassToken == "" || req.GenTime == "" || req.CaptchaOutput == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "请完成人机验证"})
|
||||
return
|
||||
}
|
||||
// TODO: 集成极验4服务端 SDK 后在这里进行二次校验
|
||||
} else if cfg.VerifyType == "geetest3" {
|
||||
if req.CaptchaOutput == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "请完成人机验证"})
|
||||
return
|
||||
}
|
||||
// TODO: 集成极验3服务端 SDK 后在这里进行二次校验
|
||||
} else if cfg.VerifyType == "sms" || cfg.VerifyType == "email" {
|
||||
if strings.TrimSpace(req.Code) == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "请输入验证码"})
|
||||
return
|
||||
}
|
||||
if err := services.VerifyBackendLoginCode(req.TenantName, req.Account, cfg.VerifyType, req.Code); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token, loginUser, err := services.BackendLogin(req.TenantName, req.Account, req.Password)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "登录成功",
|
||||
"data": map[string]interface{}{
|
||||
"token": token,
|
||||
"user": map[string]interface{}{
|
||||
"id": loginUser.ID,
|
||||
"account": loginUser.Account,
|
||||
"name": loginUser.Name,
|
||||
"tid": loginUser.Tid,
|
||||
"rid": loginUser.Rid,
|
||||
"avatar": loginUser.Avatar,
|
||||
"role_name": loginUser.RoleName,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetCurrentUser 当前登录 backend 用户信息,需 Bearer Token
|
||||
func (c *BackendAuthController) GetCurrentUser() {
|
||||
authHeader := c.Ctx.Request.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
authParts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(authParts) != 2 || authParts[0] != "Bearer" {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "认证信息格式错误"})
|
||||
return
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(authParts[1])
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "无效的token"})
|
||||
return
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
c.serveJSON(map[string]interface{}{"code": 403, "msg": "无权访问"})
|
||||
return
|
||||
}
|
||||
|
||||
var tenantUser models.SystemTenantUser
|
||||
err = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", claims.UserID).
|
||||
Filter("tid", claims.TenantId).
|
||||
One(&tenantUser)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "用户不存在"})
|
||||
return
|
||||
}
|
||||
if tenantUser.Status == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "账号已禁用"})
|
||||
return
|
||||
}
|
||||
|
||||
account := ""
|
||||
if tenantUser.Account != nil {
|
||||
account = strings.TrimSpace(*tenantUser.Account)
|
||||
}
|
||||
name := ""
|
||||
if tenantUser.Name != nil {
|
||||
name = strings.TrimSpace(*tenantUser.Name)
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"id": tenantUser.Uid,
|
||||
"account": account,
|
||||
"name": name,
|
||||
"tid": tenantUser.Tid,
|
||||
"rid": 0,
|
||||
"avatar": "",
|
||||
"role_name": "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SendLoginCode 发送 backend 登录验证码
|
||||
func (c *BackendAuthController) SendLoginCode() {
|
||||
var req struct {
|
||||
Account string `json:"account"`
|
||||
TenantName string `json:"tenant_name"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
if cfg.OpenVerifyEnabled != 1 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "当前未开启验证"})
|
||||
return
|
||||
}
|
||||
channel := strings.TrimSpace(req.Channel)
|
||||
if channel == "" {
|
||||
channel = cfg.VerifyType
|
||||
}
|
||||
if channel != "sms" && channel != "email" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "仅支持短信/邮箱验证码"})
|
||||
return
|
||||
}
|
||||
if err := services.SendBackendLoginCode(req.TenantName, req.Account, channel); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "验证码已发送"})
|
||||
}
|
||||
|
||||
// LoginBySms 手机号验证码登录(占位实现)
|
||||
func (c *BackendAuthController) LoginBySms() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 501,
|
||||
"msg": "手机号验证码登录暂未实现",
|
||||
})
|
||||
}
|
||||
|
||||
// Logout backend 退出登录(当前为无状态直接返回成功)
|
||||
func (c *BackendAuthController) Logout() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "退出成功",
|
||||
})
|
||||
}
|
||||
|
||||
// GetGeetest3Infos 获取 backend 极验3.0配置
|
||||
func (c *BackendAuthController) GetGeetest3Infos() {
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
if cfg.Geetest3ID == nil || cfg.Geetest3Key == nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 404, "msg": "未配置极验3参数"})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"captcha_id": *cfg.Geetest3ID,
|
||||
"captcha_key": *cfg.Geetest3Key,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetGeetest4Infos 获取 backend 极验4.0配置
|
||||
func (c *BackendAuthController) GetGeetest4Infos() {
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
if cfg.Geetest4ID == nil || cfg.Geetest4Key == nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 404, "msg": "未配置极验4参数"})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"captcha_id": *cfg.Geetest4ID,
|
||||
"captcha_key": *cfg.Geetest4Key,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetOpenVerify 判断是否开启 backend 登录验证
|
||||
func (c *BackendAuthController) GetOpenVerify() {
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
openVerify := "0"
|
||||
if cfg.OpenVerifyEnabled == 1 {
|
||||
openVerify = "1"
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "ok",
|
||||
"data": []map[string]string{
|
||||
{
|
||||
"label": "openVerify",
|
||||
"value": openVerify,
|
||||
},
|
||||
{
|
||||
"label": "verifyType",
|
||||
"value": cfg.VerifyType,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Register 注册(占位实现)
|
||||
func (c *BackendAuthController) Register() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 501,
|
||||
"msg": "注册暂未实现",
|
||||
})
|
||||
}
|
||||
|
||||
// SendRegisterCode 发送注册验证码(占位实现)
|
||||
func (c *BackendAuthController) SendRegisterCode() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 501,
|
||||
"msg": "发送注册验证码暂未实现",
|
||||
})
|
||||
}
|
||||
|
||||
// ResetPassword 忘记密码重置(占位实现)
|
||||
func (c *BackendAuthController) ResetPassword() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 501,
|
||||
"msg": "重置密码暂未实现",
|
||||
})
|
||||
}
|
||||
|
||||
// SendResetCode 发送找回密码验证码(占位实现)
|
||||
func (c *BackendAuthController) SendResetCode() {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 501,
|
||||
"msg": "发送找回密码验证码暂未实现",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendDomainPoolController 主域名池管理
|
||||
type BackendDomainPoolController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// BackendTenantDomainController 租户域名管理
|
||||
type BackendTenantDomainController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func requireBackend(c *beego.Controller) (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, fmt.Errorf("认证信息格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的token")
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// ===== 主域名池 =====
|
||||
|
||||
// Index GET /backend/domain/pool/index?page=&pageSize=&main_domain=&status=
|
||||
func (c *BackendDomainPoolController) Index() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
if pageSize > 200 {
|
||||
pageSize = 200
|
||||
}
|
||||
|
||||
mainDomain := strings.TrimSpace(c.GetString("main_domain"))
|
||||
statusStr := strings.TrimSpace(c.GetString("status"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemDomainPool)).Filter("delete_time__isnull", true)
|
||||
if mainDomain != "" {
|
||||
qs = qs.Filter("main_domain__icontains", mainDomain)
|
||||
}
|
||||
if statusStr != "" {
|
||||
if st, err := strconv.Atoi(statusStr); err == nil {
|
||||
qs = qs.Filter("status", st)
|
||||
}
|
||||
}
|
||||
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "获取主域名池失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var rows []models.SystemDomainPool
|
||||
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "获取主域名池失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for i := range rows {
|
||||
item := map[string]interface{}{
|
||||
"id": rows[i].ID,
|
||||
"main_domain": rows[i].MainDomain,
|
||||
"status": rows[i].Status,
|
||||
"create_time": rows[i].CreateTime.Format("2006-01-02 15:04:05"),
|
||||
"update_time": "",
|
||||
}
|
||||
if rows[i].UpdateTime != nil {
|
||||
item["update_time"] = rows[i].UpdateTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetEnabledDomains GET /backend/domain/pool/getEnabledDomains
|
||||
func (c *BackendDomainPoolController) GetEnabledDomains() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var rows []models.SystemDomainPool
|
||||
_, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "获取主域名失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
out := make([]map[string]interface{}, 0, len(rows))
|
||||
for i := range rows {
|
||||
out = append(out, map[string]interface{}{
|
||||
"id": rows[i].ID,
|
||||
"main_domain": rows[i].MainDomain,
|
||||
"status": rows[i].Status,
|
||||
})
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Create POST /backend/domain/pool/create
|
||||
func (c *BackendDomainPoolController) Create() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p domainPoolPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
md := strings.TrimSpace(p.MainDomain)
|
||||
if md == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "主域名不能为空")
|
||||
return
|
||||
}
|
||||
if p.Status != 0 && p.Status != 1 {
|
||||
p.Status = 1
|
||||
}
|
||||
// 简单去重
|
||||
cnt, _ := models.Orm.QueryTable(new(models.SystemDomainPool)).
|
||||
Filter("main_domain", md).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if cnt > 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "主域名已存在")
|
||||
return
|
||||
}
|
||||
row := &models.SystemDomainPool{MainDomain: md, Status: p.Status}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Update POST /backend/domain/pool/update
|
||||
func (c *BackendDomainPoolController) Update() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p domainPoolPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.ID == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "id 不能为空")
|
||||
return
|
||||
}
|
||||
md := strings.TrimSpace(p.MainDomain)
|
||||
if md == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "主域名不能为空")
|
||||
return
|
||||
}
|
||||
if p.Status != 0 && p.Status != 1 {
|
||||
p.Status = 1
|
||||
}
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
|
||||
Filter("id", p.ID).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"main_domain": md, "status": p.Status, "update_time": now})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
jsonErr(&c.Controller, 404, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/domain/pool/delete/:id
|
||||
func (c *BackendDomainPoolController) Delete() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
jsonErr(&c.Controller, 404, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ToggleStatus POST /backend/domain/pool/toggleStatus body:{id}
|
||||
func (c *BackendDomainPoolController) ToggleStatus() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var row models.SystemDomainPool
|
||||
if err := models.Orm.QueryTable(new(models.SystemDomainPool)).
|
||||
Filter("id", p.ID).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
newStatus := int8(1)
|
||||
if row.Status == 1 {
|
||||
newStatus = 0
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.SystemDomainPool)).
|
||||
Filter("id", p.ID).
|
||||
Update(map[string]interface{}{"status": newStatus, "update_time": now})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "切换失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ===== 租户域名 =====
|
||||
|
||||
// Index GET /backend/domain/tenant/index?page=&pageSize=&tid=&status=&sub_domain=
|
||||
func (c *BackendTenantDomainController) Index() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
if pageSize > 200 {
|
||||
pageSize = 200
|
||||
}
|
||||
|
||||
tid, _ := c.GetUint64("tid")
|
||||
statusStr := strings.TrimSpace(c.GetString("status"))
|
||||
subDomain := strings.TrimSpace(c.GetString("sub_domain"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("delete_time__isnull", true)
|
||||
if tid > 0 {
|
||||
qs = qs.Filter("tid", tid)
|
||||
}
|
||||
if statusStr != "" {
|
||||
if st, err := strconv.Atoi(statusStr); err == nil {
|
||||
qs = qs.Filter("status", st)
|
||||
}
|
||||
}
|
||||
if subDomain != "" {
|
||||
qs = qs.Filter("sub_domain__icontains", subDomain)
|
||||
}
|
||||
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "获取租户域名失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var rows []models.SystemTenantDomain
|
||||
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "获取租户域名失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
list := make([]models.SystemTenantDomain, 0, len(rows))
|
||||
list = append(list, rows...)
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": total},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MyDomains GET /backend/domain/tenant/myDomains?tid=1
|
||||
func (c *BackendTenantDomainController) MyDomains() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid, _ := c.GetUint64("tid")
|
||||
if tid == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "租户ID不能为空")
|
||||
return
|
||||
}
|
||||
var rows []models.SystemTenantDomain
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": rows}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Apply POST /backend/domain/tenant/apply body:{tid,sub_domain,main_domain}
|
||||
func (c *BackendTenantDomainController) Apply() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
SubDomain string `json:"sub_domain"`
|
||||
MainDomain string `json:"main_domain"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.Tid == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "租户ID不能为空")
|
||||
return
|
||||
}
|
||||
sub := strings.TrimSpace(p.SubDomain)
|
||||
main := strings.TrimSpace(p.MainDomain)
|
||||
if sub == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "二级域名前缀不能为空")
|
||||
return
|
||||
}
|
||||
if main == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "请选择主域名")
|
||||
return
|
||||
}
|
||||
if !subDomainRe.MatchString(sub) {
|
||||
jsonErr(&c.Controller, 400, 400, "二级域名前缀格式不正确")
|
||||
return
|
||||
}
|
||||
|
||||
// 该租户是否已有域名
|
||||
cnt, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
|
||||
Filter("tid", p.Tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if cnt > 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "该租户已有域名,请删除后再次申请")
|
||||
return
|
||||
}
|
||||
|
||||
// 主域名存在且启用
|
||||
var pool models.SystemDomainPool
|
||||
if err := models.Orm.QueryTable(new(models.SystemDomainPool)).
|
||||
Filter("main_domain", main).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&pool); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "主域名不存在或已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
// 二级域名是否已被使用(同主域名下)
|
||||
used, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
|
||||
Filter("sub_domain", sub).
|
||||
Filter("main_domain", main).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if used > 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "该二级域名已被使用")
|
||||
return
|
||||
}
|
||||
|
||||
full := sub + "." + main
|
||||
now := time.Now()
|
||||
tid := p.Tid
|
||||
row := &models.SystemTenantDomain{
|
||||
Tid: &tid,
|
||||
SubDomain: &sub,
|
||||
MainDomain: &main,
|
||||
FullDomain: &full,
|
||||
Status: 0,
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
id, err := models.Orm.Insert(row)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "申请失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "申请提交成功,等待审核", "data": map[string]interface{}{"id": uint64(id)}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Audit POST /backend/domain/tenant/audit body:{id,action} action=approve/reject
|
||||
func (c *BackendTenantDomainController) Audit() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var row models.SystemTenantDomain
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).One(&row); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "域名不存在")
|
||||
return
|
||||
}
|
||||
if row.Status != 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "该域名已审核过了")
|
||||
return
|
||||
}
|
||||
newStatus := 2
|
||||
msg := "已拒绝"
|
||||
if strings.ToLower(strings.TrimSpace(p.Action)) == "approve" {
|
||||
newStatus = 1
|
||||
msg = "审核通过"
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).Update(map[string]interface{}{
|
||||
"status": newStatus,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "审核失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ToggleStatus POST /backend/domain/tenant/toggleStatus body:{id}
|
||||
func (c *BackendTenantDomainController) ToggleStatus() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var row models.SystemTenantDomain
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).One(&row); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "域名不存在")
|
||||
return
|
||||
}
|
||||
if row.Status == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "审核中不可操作")
|
||||
return
|
||||
}
|
||||
newStatus := 2
|
||||
if row.Status == 2 {
|
||||
newStatus = 1
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).Update(map[string]interface{}{
|
||||
"status": newStatus,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "操作失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/domain/tenant/delete/:id
|
||||
func (c *BackendTenantDomainController) Delete() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
jsonErr(&c.Controller, 404, 404, "域名不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// 用于复杂筛选时可扩展:当前保留 orm.Condition import,避免被 gofmt 删除
|
||||
var _ = orm.NewCondition
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,907 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendFileController 平台端文件管理(yz_system_files / yz_system_files_category)
|
||||
type BackendFileController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
const fileUploadMaxMB = 2048 // 2GB,适用于大型软件安装包
|
||||
const fileUploadMaxBytes = fileUploadMaxMB * 1024 * 1024
|
||||
|
||||
var fileTypeByCategory = map[string]uint8{
|
||||
"image": 1,
|
||||
"document": 2,
|
||||
"video": 3,
|
||||
"audio": 4,
|
||||
"appsupgrade": 2,
|
||||
}
|
||||
|
||||
var allowedExtByCategory = map[string][]string{
|
||||
"image": {"jpg", "jpeg", "png", "gif", "bmp", "webp"},
|
||||
"document": {"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt"},
|
||||
"video": {"mp4", "webm", "mov"},
|
||||
"audio": {"mp3", "wav", "ogg"},
|
||||
// 安装包 / 软件升级(上传时 cate 选 appsupgrade 分类即可,扩展名在此放行)
|
||||
"appsupgrade": {"zip", "exe", "dmg", "msi", "msix", "apk", "deb", "rpm", "7z", "tar", "gz", "pkg"},
|
||||
}
|
||||
|
||||
func (c *BackendFileController) backendClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, fmt.Errorf("认证信息格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的token")
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *BackendFileController) effectiveTid(claims *jwtutil.Claims) uint64 {
|
||||
_ = c.ParseForm(1 << 20)
|
||||
if tid, err := c.GetUint64("tid"); err == nil && tid > 0 {
|
||||
return tid
|
||||
}
|
||||
if h := strings.TrimSpace(c.Ctx.Request.Header.Get("X-Tenant-Id")); h != "" {
|
||||
if v, e := strconv.ParseUint(h, 10, 64); e == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
if claims != nil && claims.TenantId > 0 {
|
||||
return uint64(claims.TenantId)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *BackendFileController) 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 *BackendFileController) jsonOK(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func detectFileType(ext string) uint8 {
|
||||
ext = strings.ToLower(strings.TrimPrefix(ext, "."))
|
||||
for cat, exts := range allowedExtByCategory {
|
||||
for _, e := range exts {
|
||||
if e == ext {
|
||||
if t, ok := fileTypeByCategory[cat]; ok {
|
||||
return t
|
||||
}
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
func fileExt(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if i := strings.LastIndex(name, "."); i >= 0 && i < len(name)-1 {
|
||||
return strings.ToLower(name[i+1:])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func fileToMap(f *models.SystemFile) map[string]interface{} {
|
||||
ct := f.CreateTime.Format("2006-01-02 15:04:05")
|
||||
m := map[string]interface{}{
|
||||
"id": f.ID,
|
||||
"tid": f.Tid,
|
||||
"name": f.Name,
|
||||
"type": f.Type,
|
||||
"cate": f.Cate,
|
||||
"size": f.Size,
|
||||
"src": f.Src,
|
||||
"uploader": f.Uploader,
|
||||
"md5": f.Md5,
|
||||
"create_time": ct,
|
||||
"createTime": ct,
|
||||
"groupId": f.Cate,
|
||||
"url": f.Src,
|
||||
}
|
||||
if f.Uid != nil {
|
||||
m["uid"] = *f.Uid
|
||||
}
|
||||
if f.Tuid != nil {
|
||||
m["tuid"] = *f.Tuid
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func removePhysicalBySrc(webSrc string) {
|
||||
webSrc = strings.TrimSpace(webSrc)
|
||||
if webSrc == "" {
|
||||
return
|
||||
}
|
||||
webSrc = strings.TrimPrefix(webSrc, "/")
|
||||
_ = os.Remove(webSrc)
|
||||
}
|
||||
|
||||
// GetAllFiles GET /backend/allfiles
|
||||
func (c *BackendFileController) GetAllFiles() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
cate, _ := c.GetUint64("cate")
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true)
|
||||
if cate > 0 {
|
||||
qs = qs.Filter("cate", cate)
|
||||
}
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("name__icontains", keyword)
|
||||
}
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取文件列表失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var rows []models.SystemFile
|
||||
_, err = qs.OrderBy("-create_time").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, fileToMap(&rows[i]))
|
||||
}
|
||||
c.jsonOK(map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pageSize": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserCate GET /backend/usercate
|
||||
func (c *BackendFileController) GetUserCate() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
|
||||
var cates []models.SystemFilesCategory
|
||||
_, err = models.Orm.QueryTable(new(models.SystemFilesCategory)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("id").
|
||||
All(&cates)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取用户分类失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
out := make([]map[string]interface{}, 0, len(cates))
|
||||
for i := range cates {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("tid", tid).
|
||||
Filter("cate", cates[i].ID).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
out = append(out, map[string]interface{}{
|
||||
"id": cates[i].ID,
|
||||
"name": cates[i].Name,
|
||||
"total": cnt,
|
||||
})
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type createCateBody struct {
|
||||
Name string `json:"name"`
|
||||
Tuid *uint64 `json:"tuid"`
|
||||
}
|
||||
|
||||
// CreateFileCate POST /backend/createfilecate
|
||||
func (c *BackendFileController) CreateFileCate() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body createCateBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(body.Name)
|
||||
if name == "" {
|
||||
c.jsonErr(400, 400, "分组名称不能为空")
|
||||
return
|
||||
}
|
||||
uid := uint64(claims.UserID)
|
||||
row := &models.SystemFilesCategory{
|
||||
Tid: tid,
|
||||
Name: name,
|
||||
Uid: &uid,
|
||||
Tuid: body.Tuid,
|
||||
}
|
||||
id, err := models.Orm.Insert(row)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "新建文件分组失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "新建文件分组成功",
|
||||
"data": map[string]interface{}{"id": uint64(id)},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type renameCateBody struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// RenameFileCate POST /backend/renamefilecate/:id
|
||||
func (c *BackendFileController) RenameFileCate() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效的分组ID")
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body renameCateBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(body.Name)
|
||||
if name == "" {
|
||||
c.jsonErr(400, 400, "分组名称不能为空")
|
||||
return
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFilesCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"name": name})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "重命名文件分组失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.jsonErr(404, 404, "分组不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "重命名文件分组成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteFileCate DELETE /backend/deletefilecate/:id
|
||||
func (c *BackendFileController) DeleteFileCate() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效的分组ID")
|
||||
return
|
||||
}
|
||||
cnt, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("cate", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "删除文件分组失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if cnt > 0 {
|
||||
c.jsonErr(400, 400, fmt.Sprintf("该分组下还有 %d 个文件,请先删除分组内文件!", cnt))
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFilesCategory)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
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.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除文件分组成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetCateFiles GET /backend/catefiles/:id
|
||||
func (c *BackendFileController) GetCateFiles() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
cateID, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "无效的分类ID")
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 24)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 24
|
||||
}
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("tid", tid).
|
||||
Filter("cate", cateID).
|
||||
Filter("delete_time__isnull", true)
|
||||
if keyword != "" {
|
||||
qs = qs.Filter("name__icontains", keyword)
|
||||
}
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取分类文件失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var rows []models.SystemFile
|
||||
_, err = qs.OrderBy("-create_time").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, fileToMap(&rows[i]))
|
||||
}
|
||||
c.jsonOK(map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pageSize": pageSize,
|
||||
"categoryId": cateID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetFileByID GET /backend/file/:id
|
||||
func (c *BackendFileController) GetFileByID() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效的文件ID")
|
||||
return
|
||||
}
|
||||
var f models.SystemFile
|
||||
err = models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&f)
|
||||
if err != nil {
|
||||
c.jsonErr(404, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
c.jsonOK(fileToMap(&f))
|
||||
}
|
||||
|
||||
// UploadFile POST /backend/uploadfile
|
||||
func (c *BackendFileController) UploadFile() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
if err := c.Ctx.Request.ParseMultipartForm(fileUploadMaxBytes); err != nil {
|
||||
c.jsonErr(400, 400, "解析上传失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
fh, header, err := c.GetFile("file")
|
||||
if err != nil || fh == nil {
|
||||
c.jsonErr(400, 400, "请选择要上传的文件")
|
||||
return
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
if header != nil && header.Size > fileUploadMaxBytes {
|
||||
c.jsonErr(400, 400, fmt.Sprintf("文件大小不能超过%dMB", fileUploadMaxMB))
|
||||
return
|
||||
}
|
||||
|
||||
ext := fileExt(header.Filename)
|
||||
if ext == "" {
|
||||
c.jsonErr(400, 400, "无法识别文件扩展名")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取存储服务
|
||||
storageService, err := services.GetStorageService()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取存储服务失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
result, err := storageService.Upload(fh, header)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "上传文件失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否已存在(通过MD5)
|
||||
var exist models.SystemFile
|
||||
err = models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("md5", result.MD5).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&exist)
|
||||
if err == nil {
|
||||
// 文件已存在,返回已有记录
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 201,
|
||||
"msg": "文件已存在",
|
||||
"data": map[string]interface{}{
|
||||
"url": exist.Src,
|
||||
"id": exist.ID,
|
||||
"name": exist.Name,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取分类
|
||||
cateStr := c.GetString("cate")
|
||||
var cate uint64
|
||||
if cateStr != "" {
|
||||
cate, _ = strconv.ParseUint(cateStr, 10, 64)
|
||||
}
|
||||
|
||||
adminID := uint64(claims.UserID)
|
||||
var tuidPtr *uint64
|
||||
if ts := strings.TrimSpace(c.GetString("tuid")); ts != "" {
|
||||
if v, e := strconv.ParseUint(ts, 10, 64); e == nil {
|
||||
tuidPtr = &v
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件记录到数据库
|
||||
row := &models.SystemFile{
|
||||
Tid: tid,
|
||||
Uid: &adminID,
|
||||
Tuid: tuidPtr,
|
||||
Name: header.Filename,
|
||||
Type: detectFileType(ext),
|
||||
Cate: cate,
|
||||
Size: uint64(result.Size),
|
||||
Src: result.URL,
|
||||
Uploader: adminID,
|
||||
Md5: result.MD5,
|
||||
}
|
||||
id, err := models.Orm.Insert(row)
|
||||
if err != nil {
|
||||
// 数据库插入失败,尝试删除已上传的文件
|
||||
_ = storageService.Delete(result.Key)
|
||||
c.jsonErr(500, 500, "上传失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "上传成功",
|
||||
"data": map[string]interface{}{
|
||||
"url": result.URL,
|
||||
"id": uint64(id),
|
||||
"name": header.Filename,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func md5HashFile(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
h := md5.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
type updateFileBody struct {
|
||||
Name *string `json:"name"`
|
||||
Cate *uint64 `json:"cate"`
|
||||
}
|
||||
|
||||
// UpdateFile POST /backend/updatefile/:id
|
||||
func (c *BackendFileController) UpdateFile() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效的文件ID")
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body updateFileBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
up := map[string]interface{}{}
|
||||
if body.Name != nil {
|
||||
up["name"] = strings.TrimSpace(*body.Name)
|
||||
}
|
||||
if body.Cate != nil {
|
||||
up["cate"] = *body.Cate
|
||||
}
|
||||
if len(up) == 0 {
|
||||
c.jsonErr(400, 400, "无更新数据")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
up["update_time"] = now
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(up)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.jsonErr(404, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteFile DELETE /backend/deletefile/:id
|
||||
func (c *BackendFileController) DeleteFile() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效的文件ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
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.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteFilePermanently DELETE /backend/deletefilepermanently/:id
|
||||
func (c *BackendFileController) DeleteFilePermanently() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效的文件ID")
|
||||
return
|
||||
}
|
||||
var f models.SystemFile
|
||||
err = models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
One(&f)
|
||||
if err != nil {
|
||||
c.jsonErr(404, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
removePhysicalBySrc(f.Src)
|
||||
_, err = models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Delete()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "永久删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "永久删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// MoveFile GET /backend/movefile/:id
|
||||
func (c *BackendFileController) MoveFile() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效的文件ID")
|
||||
return
|
||||
}
|
||||
cate, _ := c.GetUint64("cate")
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"cate": cate, "update_time": now})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "移动失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.jsonErr(404, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "移动成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type idsBody struct {
|
||||
IDs []uint64 `json:"ids"`
|
||||
Cate *uint64 `json:"cate"`
|
||||
}
|
||||
|
||||
// BatchDeleteFiles POST /backend/batchdeletefiles
|
||||
func (c *BackendFileController) BatchDeleteFiles() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body idsBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if len(body.IDs) == 0 {
|
||||
c.jsonErr(400, 400, "请选择要删除的文件")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
for _, id := range body.IDs {
|
||||
var f models.SystemFile
|
||||
e := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
One(&f)
|
||||
if e == nil && f.Src != "" {
|
||||
removePhysicalBySrc(f.Src)
|
||||
}
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id__in", body.IDs).
|
||||
Filter("tid", tid).
|
||||
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.Data["json"] = map[string]interface{}{"code": 200, "msg": "批量删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// BatchDeleteFilesPermanently POST /backend/batchDeleteFilesPermanently
|
||||
func (c *BackendFileController) BatchDeleteFilesPermanently() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body idsBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if len(body.IDs) == 0 {
|
||||
c.jsonErr(400, 400, "请选择要彻底删除的文件")
|
||||
return
|
||||
}
|
||||
var rows []models.SystemFile
|
||||
_, err = models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id__in", body.IDs).
|
||||
Filter("tid", tid).
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "批量彻底删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
for i := range rows {
|
||||
removePhysicalBySrc(rows[i].Src)
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id__in", body.IDs).
|
||||
Filter("tid", tid).
|
||||
Delete()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "批量彻底删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.jsonErr(404, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "批量彻底删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// UploadAvatar POST /backend/uploadavatar(占位)
|
||||
func (c *BackendFileController) UploadAvatar() {
|
||||
c.Data["json"] = map[string]interface{}{"code": 501, "msg": "上传头像暂未实现"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateAvatar POST /backend/uploadavatar/:id(占位)
|
||||
func (c *BackendFileController) UpdateAvatar() {
|
||||
c.Data["json"] = map[string]interface{}{"code": 501, "msg": "更新头像暂未实现"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// BatchMoveFiles POST /backend/batchMoveFiles
|
||||
func (c *BackendFileController) BatchMoveFiles() {
|
||||
claims, err := c.backendClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body idsBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if len(body.IDs) == 0 {
|
||||
c.jsonErr(400, 400, "请选择要移动的文件")
|
||||
return
|
||||
}
|
||||
if body.Cate == nil {
|
||||
c.jsonErr(400, 400, "缺少目标分类")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id__in", body.IDs).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"cate": *body.Cate, "update_time": now})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "批量移动失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
c.jsonErr(404, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "批量移动成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendLoginVerifyController 后台登录验证配置
|
||||
// 对应前端 backend/src/api/sitesettings.js:
|
||||
// - GET /backend/loginVerifyInfos
|
||||
// - POST /backend/saveloginVerifyInfos
|
||||
type BackendLoginVerifyController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendLoginVerifyController) backendLoginVerifyClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, errBackendLoginVerify("未登录")
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, errBackendLoginVerify("认证信息格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, errBackendLoginVerify("无效的token")
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
return nil, errBackendLoginVerify("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
type backendLoginVerifyError string
|
||||
|
||||
func (e backendLoginVerifyError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func errBackendLoginVerify(msg string) error {
|
||||
return backendLoginVerifyError(msg)
|
||||
}
|
||||
|
||||
func (c *BackendLoginVerifyController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type backendLoginVerifyPayload struct {
|
||||
OpenVerify *bool `json:"openVerify"`
|
||||
OpenVerifyInt *int8 `json:"openVerify_enabled"`
|
||||
VerifyModel string `json:"verifyModel"`
|
||||
UseGeetest string `json:"use_geetest"`
|
||||
Geetest3ID *string `json:"geetest3ID"`
|
||||
Geetest3IDSnake *string `json:"geetest3_id"`
|
||||
Geetest3Key *string `json:"geetest3KEY"`
|
||||
Geetest3KeySnake *string `json:"geetest3_key"`
|
||||
Geetest4ID *string `json:"geetest4ID"`
|
||||
Geetest4IDSnake *string `json:"geetest4_id"`
|
||||
Geetest4Key *string `json:"geetest4KEY"`
|
||||
Geetest4KeySnake *string `json:"geetest4_key"`
|
||||
}
|
||||
|
||||
func backendVerifyTypeToModel(v string) string {
|
||||
switch strings.TrimSpace(v) {
|
||||
case "captcha":
|
||||
return "1"
|
||||
case "sms":
|
||||
return "2"
|
||||
case "email":
|
||||
return "3"
|
||||
case "geetest3":
|
||||
return "4"
|
||||
case "geetest", "geetest4":
|
||||
return "5"
|
||||
default:
|
||||
return "1"
|
||||
}
|
||||
}
|
||||
|
||||
func backendVerifyModelToType(v string) string {
|
||||
switch strings.TrimSpace(v) {
|
||||
case "1":
|
||||
return "captcha"
|
||||
case "2":
|
||||
return "sms"
|
||||
case "3":
|
||||
return "email"
|
||||
case "4":
|
||||
return "geetest3"
|
||||
case "5":
|
||||
return "geetest4"
|
||||
default:
|
||||
switch strings.TrimSpace(v) {
|
||||
case "captcha", "sms", "email", "geetest", "geetest3", "geetest4":
|
||||
return strings.TrimSpace(v)
|
||||
default:
|
||||
return "captcha"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func backendStringPtrValue(primary, fallback *string) string {
|
||||
if primary != nil {
|
||||
return *primary
|
||||
}
|
||||
if fallback != nil {
|
||||
return *fallback
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func backendStringPtrOrNil(primary, fallback *string) *string {
|
||||
value := strings.TrimSpace(backendStringPtrValue(primary, fallback))
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
// GetLoginVerifyInfos GET /backend/loginVerifyInfos
|
||||
func (c *BackendLoginVerifyController) GetLoginVerifyInfos() {
|
||||
if _, err := c.backendLoginVerifyClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := models.GetPlatformLoginVerify()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
openVerify := "0"
|
||||
if cfg.OpenVerifyEnabled == 1 {
|
||||
openVerify = "1"
|
||||
}
|
||||
|
||||
data := []map[string]string{
|
||||
{"label": "openVerify", "value": openVerify},
|
||||
{"label": "verifyModel", "value": backendVerifyTypeToModel(cfg.VerifyType)},
|
||||
{"label": "geetest3ID", "value": backendStringPtrValue(cfg.Geetest3ID, nil)},
|
||||
{"label": "geetest3KEY", "value": backendStringPtrValue(cfg.Geetest3Key, nil)},
|
||||
{"label": "geetest4ID", "value": backendStringPtrValue(cfg.Geetest4ID, nil)},
|
||||
{"label": "geetest4KEY", "value": backendStringPtrValue(cfg.Geetest4Key, nil)},
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// SaveLoginVerifyInfos POST /backend/saveloginVerifyInfos
|
||||
func (c *BackendLoginVerifyController) SaveLoginVerifyInfos() {
|
||||
if _, err := c.backendLoginVerifyClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var p backendLoginVerifyPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
openVerifyEnabled := int8(0)
|
||||
if p.OpenVerify != nil && *p.OpenVerify {
|
||||
openVerifyEnabled = 1
|
||||
}
|
||||
if p.OpenVerifyInt != nil {
|
||||
openVerifyEnabled = *p.OpenVerifyInt
|
||||
}
|
||||
|
||||
verifyModel := p.VerifyModel
|
||||
if strings.TrimSpace(verifyModel) == "" {
|
||||
verifyModel = p.UseGeetest
|
||||
}
|
||||
verifyType := backendVerifyModelToType(verifyModel)
|
||||
|
||||
geetest3ID := backendStringPtrOrNil(p.Geetest3ID, p.Geetest3IDSnake)
|
||||
geetest3Key := backendStringPtrOrNil(p.Geetest3Key, p.Geetest3KeySnake)
|
||||
geetest4ID := backendStringPtrOrNil(p.Geetest4ID, p.Geetest4IDSnake)
|
||||
geetest4Key := backendStringPtrOrNil(p.Geetest4Key, p.Geetest4KeySnake)
|
||||
|
||||
if verifyType == "geetest3" {
|
||||
if geetest3ID == nil || geetest3Key == nil {
|
||||
c.jsonErr(400, 400, "极验3.0 ID和KEY不能为空")
|
||||
return
|
||||
}
|
||||
}
|
||||
if verifyType == "geetest4" || verifyType == "geetest" {
|
||||
if geetest4ID == nil || geetest4Key == nil {
|
||||
c.jsonErr(400, 400, "极验4.0 ID和KEY不能为空")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var existed models.PlatformLoginVerify
|
||||
err = models.Orm.QueryTable(new(models.PlatformLoginVerify)).OrderBy("-id").One(&existed)
|
||||
if err == nil {
|
||||
_, err = models.Orm.QueryTable(new(models.PlatformLoginVerify)).
|
||||
Filter("id", existed.ID).
|
||||
Update(map[string]interface{}{
|
||||
"open_verify_enabled": openVerifyEnabled,
|
||||
"verify_type": verifyType,
|
||||
"geetest3_id": geetest3ID,
|
||||
"geetest3_key": geetest3Key,
|
||||
"geetest4_id": geetest4ID,
|
||||
"geetest4_key": geetest4Key,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
row := &models.PlatformLoginVerify{
|
||||
OpenVerifyEnabled: openVerifyEnabled,
|
||||
VerifyType: verifyType,
|
||||
Geetest3ID: geetest3ID,
|
||||
Geetest3Key: geetest3Key,
|
||||
Geetest4ID: geetest4ID,
|
||||
Geetest4Key: geetest4Key,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
c.jsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
+86
-167
@@ -2,7 +2,6 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"server/models"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -10,11 +9,12 @@ import (
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// AdminMenuController 后台菜单控制器
|
||||
type AdminMenuController struct {
|
||||
type BackendMenuController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type AdminMenuController = BackendMenuController
|
||||
|
||||
type menuPayload struct {
|
||||
Pid *int64 `json:"pid"`
|
||||
Title *string `json:"title"`
|
||||
@@ -30,15 +30,11 @@ type menuPayload struct {
|
||||
}
|
||||
|
||||
func parseViews(raw *string) []int {
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
s := strings.TrimSpace(*raw)
|
||||
if s == "" {
|
||||
if raw == nil || strings.TrimSpace(*raw) == "" {
|
||||
return nil
|
||||
}
|
||||
var arr []int
|
||||
if err := json.Unmarshal([]byte(s), &arr); err != nil {
|
||||
if err := json.Unmarshal([]byte(*raw), &arr); err != nil {
|
||||
return nil
|
||||
}
|
||||
return arr
|
||||
@@ -53,21 +49,10 @@ func hasView(arr []int, v int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func viewsJSON(views []int) string {
|
||||
// 默认:平台端显示
|
||||
if len(views) == 0 {
|
||||
views = []int{1}
|
||||
}
|
||||
b, _ := json.Marshal(views)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func filterMenusByView(menus []models.SystemMenu, v int) []models.SystemMenu {
|
||||
out := make([]models.SystemMenu, 0, len(menus))
|
||||
for _, m := range menus {
|
||||
views := parseViews(m.Views)
|
||||
// 兼容旧数据:views 为空时,平台端菜单默认可见(保持旧 is_platform=1 的常见默认体验)
|
||||
// 租户端不做默认放行,避免把未迁移数据误暴露到租户端。
|
||||
if v == 1 && len(views) == 0 {
|
||||
out = append(out, m)
|
||||
continue
|
||||
@@ -79,87 +64,63 @@ func filterMenusByView(menus []models.SystemMenu, v int) []models.SystemMenu {
|
||||
return out
|
||||
}
|
||||
|
||||
// GetMenu 获取指定用户可见的菜单列表(简化版:当前先忽略用户权限,返回全部启用且平台端菜单)
|
||||
// 路由示例:GET /platform/menu/1
|
||||
func (c *AdminMenuController) GetMenu() {
|
||||
// 从路由参数中解析用户 ID,占位保留,方便后续按用户权限过滤
|
||||
_ = c.Ctx.Input.Param(":id")
|
||||
|
||||
// 查询所有启用菜单,再按 views 过滤平台端可见
|
||||
func (c *BackendMenuController) GetMenu() {
|
||||
var menus []models.SystemMenu
|
||||
qs := models.Orm.
|
||||
QueryTable(new(models.SystemMenu)).
|
||||
Filter("status", 1)
|
||||
_, err := qs.All(&menus)
|
||||
_, err := models.Orm.QueryTable(new(models.SystemMenu)).Filter("status", 1).All(&menus)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 500,
|
||||
"msg": "获取菜单失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
menus = filterMenusByView(menus, 1)
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(filterMenusByView(menus, 1), 0)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// 将平铺的菜单列表构建为树形结构
|
||||
menuTree := buildMenuTree(menus, 0)
|
||||
func (c *BackendMenuController) GetBackendMenu() {
|
||||
var menus []models.SystemMenu
|
||||
_, err := models.Orm.QueryTable(new(models.SystemMenu)).Filter("status", 1).All(&menus)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(filterMenusByView(menus, 2), 0)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendMenuController) GetTenantList() {
|
||||
var tid uint64
|
||||
if jwtTid := c.Ctx.Input.GetData("tid"); jwtTid != nil {
|
||||
tid = jwtTid.(uint64)
|
||||
}
|
||||
if tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "未登录或非法请求"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var menus []models.SystemMenu
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).Filter("status", 1).All(&menus); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取失败:" + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
tree := buildMenuTree(filterMenusByView(menus, 2), 0)
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": menuTree,
|
||||
"msg": "获取成功",
|
||||
"data": map[string]interface{}{"list": tree, "total": len(tree)},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetBackendMenu 获取租户端用户可见的菜单列表(简化版:当前先忽略用户权限,返回全部启用且租户端菜单)
|
||||
// 路由示例:GET /backend/menu/1
|
||||
func (c *AdminMenuController) GetBackendMenu() {
|
||||
// 从路由参数中解析用户 ID,占位保留,方便后续按用户权限过滤
|
||||
_ = c.Ctx.Input.Param(":id")
|
||||
|
||||
var menus []models.SystemMenu
|
||||
qs := models.Orm.
|
||||
QueryTable(new(models.SystemMenu)).
|
||||
Filter("status", 1)
|
||||
_, err := qs.All(&menus)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 500,
|
||||
"msg": "获取菜单失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
menus = filterMenusByView(menus, 2)
|
||||
|
||||
menuTree := buildMenuTree(menus, 0)
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": menuTree,
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetAllMenus 获取平台端全部菜单(用于菜单管理界面)
|
||||
// 路由:GET /platform/allmenu
|
||||
func (c *AdminMenuController) GetAllMenus() {
|
||||
func (c *BackendMenuController) GetAllMenus() {
|
||||
var menus []models.SystemMenu
|
||||
cid, _ := c.GetInt("cid")
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemMenu))
|
||||
// 菜单管理默认返回全量菜单;仅在明确传 cid 时按分类筛选
|
||||
// cid: 1平台角色 -> 平台菜单;2租户角色 -> 租户菜单
|
||||
_, err := qs.All(&menus)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 500,
|
||||
"msg": "获取菜单失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
@@ -169,42 +130,21 @@ func (c *AdminMenuController) GetAllMenus() {
|
||||
menus = filterMenusByView(menus, 2)
|
||||
}
|
||||
|
||||
tree := buildMenuTree(menus, 0)
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": tree,
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(menus, 0)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetAllBackendMenus 获取租户端全部菜单(用于菜单管理界面)
|
||||
// 路由:GET /backend/allmenu
|
||||
func (c *AdminMenuController) GetAllBackendMenus() {
|
||||
func (c *BackendMenuController) GetAllBackendMenus() {
|
||||
var menus []models.SystemMenu
|
||||
_, err := models.Orm.QueryTable(new(models.SystemMenu)).
|
||||
All(&menus)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 500,
|
||||
"msg": "获取菜单失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
menus = filterMenusByView(menus, 2)
|
||||
tree := buildMenuTree(menus, 0)
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": tree,
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(filterMenusByView(menus, 2), 0)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// menuNode 用于 JSON 返回的菜单结构
|
||||
type menuNode struct {
|
||||
ID uint64 `json:"id"`
|
||||
Pid int64 `json:"pid"`
|
||||
@@ -221,7 +161,6 @@ type menuNode struct {
|
||||
Children []*menuNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
// buildMenuTree 将菜单列表构建成树结构
|
||||
func buildMenuTree(menus []models.SystemMenu, pid int64) []*menuNode {
|
||||
var tree []*menuNode
|
||||
for _, m := range menus {
|
||||
@@ -248,10 +187,7 @@ func buildMenuTree(menus []models.SystemMenu, pid int64) []*menuNode {
|
||||
if m.Permission != nil {
|
||||
node.Permission = *m.Permission
|
||||
}
|
||||
|
||||
// 递归查找子菜单
|
||||
children := buildMenuTree(menus, int64(m.ID))
|
||||
if len(children) > 0 {
|
||||
if children := buildMenuTree(menus, int64(m.ID)); len(children) > 0 {
|
||||
node.Children = children
|
||||
}
|
||||
tree = append(tree, node)
|
||||
@@ -260,9 +196,7 @@ func buildMenuTree(menus []models.SystemMenu, pid int64) []*menuNode {
|
||||
return tree
|
||||
}
|
||||
|
||||
// UpdateMenuStatus 更新菜单状态
|
||||
// 路由:PATCH /platform/menu/status/:id
|
||||
func (c *AdminMenuController) UpdateMenuStatus() {
|
||||
func (c *BackendMenuController) UpdateMenuStatus() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效菜单ID"}
|
||||
@@ -273,67 +207,61 @@ func (c *AdminMenuController) UpdateMenuStatus() {
|
||||
var body struct {
|
||||
Status *int8 `json:"status"`
|
||||
}
|
||||
rawBody, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(rawBody, &body); err != nil || body.Status == nil {
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &body); err != nil || body.Status == nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.SystemMenu)).
|
||||
Filter("id", id).
|
||||
Update(map[string]interface{}{"status": *body.Status})
|
||||
if err != nil {
|
||||
if _, err = models.Orm.QueryTable(new(models.SystemMenu)).Filter("id", id).Update(map[string]interface{}{"status": *body.Status}); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "success": true}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// CreateMenu 创建菜单
|
||||
// 路由:POST /platform/createmenu
|
||||
func (c *AdminMenuController) CreateMenu() {
|
||||
func (c *BackendMenuController) CreateMenu() {
|
||||
payload, ok := c.parseMenuPayload(true)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var viewsStr string
|
||||
views := payload.Views
|
||||
if len(views) == 0 {
|
||||
views = []int{1}
|
||||
}
|
||||
if b, err := json.Marshal(views); err == nil {
|
||||
viewsStr = string(b)
|
||||
}
|
||||
|
||||
menu := models.SystemMenu{
|
||||
Pid: valueInt64(payload.Pid, 0),
|
||||
Title: strings.TrimSpace(valueString(payload.Title, "")),
|
||||
Sort: valueInt64(payload.Sort, 0),
|
||||
Status: valueInt8(payload.Status, 1),
|
||||
IsVisible: ptrInt8(valueInt8(payload.IsVisible, 1)),
|
||||
Views: ptrString(viewsJSON(payload.Views)),
|
||||
Views: &viewsStr,
|
||||
Type: valueInt8(payload.Type, 1),
|
||||
Path: ptrString(valueString(payload.Path, "")),
|
||||
ComponentPath: ptrString(valueString(payload.ComponentPath, "")),
|
||||
Icon: ptrString(valueString(payload.Icon, "")),
|
||||
Permission: ptrString(valueString(payload.Permission, "")),
|
||||
}
|
||||
|
||||
menu.Path = ptrString(valueString(payload.Path, ""))
|
||||
menu.ComponentPath = ptrString(valueString(payload.ComponentPath, ""))
|
||||
menu.Icon = ptrString(valueString(payload.Icon, ""))
|
||||
menu.Permission = ptrString(valueString(payload.Permission, ""))
|
||||
|
||||
id, err := models.Orm.Insert(&menu)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "创建成功",
|
||||
"data": map[string]interface{}{"id": id},
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateMenu 更新菜单
|
||||
// 路由:PUT /platform/updatemenu/:id
|
||||
func (c *AdminMenuController) UpdateMenu() {
|
||||
func (c *BackendMenuController) UpdateMenu() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效菜单ID"}
|
||||
@@ -346,6 +274,12 @@ func (c *AdminMenuController) UpdateMenu() {
|
||||
return
|
||||
}
|
||||
|
||||
views := payload.Views
|
||||
if len(views) == 0 {
|
||||
views = []int{1}
|
||||
}
|
||||
viewsBytes, _ := json.Marshal(views)
|
||||
|
||||
update := map[string]interface{}{
|
||||
"pid": valueInt64(payload.Pid, 0),
|
||||
"title": strings.TrimSpace(valueString(payload.Title, "")),
|
||||
@@ -355,27 +289,21 @@ func (c *AdminMenuController) UpdateMenu() {
|
||||
"sort": valueInt64(payload.Sort, 0),
|
||||
"status": valueInt8(payload.Status, 1),
|
||||
"is_visible": valueInt8(payload.IsVisible, 1),
|
||||
"views": viewsJSON(payload.Views),
|
||||
"views": string(viewsBytes),
|
||||
"type": valueInt8(payload.Type, 1),
|
||||
"permission": valueString(payload.Permission, ""),
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.SystemMenu)).
|
||||
Filter("id", id).
|
||||
Update(update)
|
||||
if err != nil {
|
||||
if _, err = models.Orm.QueryTable(new(models.SystemMenu)).Filter("id", id).Update(update); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteMenu 删除菜单
|
||||
// 路由:DELETE /platform/deletemenu/:id
|
||||
func (c *AdminMenuController) DeleteMenu() {
|
||||
func (c *BackendMenuController) DeleteMenu() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效菜单ID"}
|
||||
@@ -383,21 +311,18 @@ func (c *AdminMenuController) DeleteMenu() {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.SystemMenu)).Filter("id", id).Delete()
|
||||
if err != nil {
|
||||
if _, err = models.Orm.QueryTable(new(models.SystemMenu)).Filter("id", id).Delete(); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功", "success": true}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *AdminMenuController) parseMenuPayload(needTitle bool) (*menuPayload, bool) {
|
||||
rawBody, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
func (c *BackendMenuController) parseMenuPayload(needTitle bool) (*menuPayload, bool) {
|
||||
var payload menuPayload
|
||||
if err := json.Unmarshal(rawBody, &payload); err != nil {
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||||
_ = c.ServeJSON()
|
||||
return nil, false
|
||||
@@ -431,11 +356,5 @@ func valueInt64(v *int64, def int64) int64 {
|
||||
return *v
|
||||
}
|
||||
|
||||
func ptrString(v string) *string {
|
||||
return &v
|
||||
}
|
||||
|
||||
func ptrInt8(v int8) *int8 {
|
||||
return &v
|
||||
}
|
||||
|
||||
func ptrString(v string) *string { return &v }
|
||||
func ptrInt8(v int8) *int8 { return &v }
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendModulesController backend 模块接口(yz_system_modules)
|
||||
type BackendModulesController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendModulesController) backendModulesClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, fmt.Errorf("认证信息格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的token")
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *BackendModulesController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantList GET /backend/modules/getTenantList
|
||||
// 返回当前 backend 账号可见的模块。当前实现:返回 status=1 且 is_show=1 的全部模块。
|
||||
func (c *BackendModulesController) GetTenantList() {
|
||||
if _, err := c.backendModulesClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var rows []models.SystemModules
|
||||
_, err := models.Orm.QueryTable(new(models.SystemModules)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("status", 1).
|
||||
Filter("is_show", 1).
|
||||
OrderBy("sort", "id").
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取失败:"+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "获取成功",
|
||||
"data": map[string]interface{}{
|
||||
"list": rows,
|
||||
"total": len(rows),
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// BackendOperationLogController 操作日志(yz_system_operation_log)
|
||||
type BackendOperationLogController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendOperationLogController) backendClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, fmt.Errorf("认证信息格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的token")
|
||||
}
|
||||
if claims.UserType != "backend" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (c *BackendOperationLogController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// List GET /backend/operationLogs?page=1&pageSize=20&keyword=&module=&action=&status=&startTime=&endTime=
|
||||
func (c *BackendOperationLogController) List() {
|
||||
if _, err := c.backendClaims(); 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"))
|
||||
module := strings.TrimSpace(c.GetString("module"))
|
||||
action := strings.TrimSpace(c.GetString("action"))
|
||||
statusStr := strings.TrimSpace(c.GetString("status"))
|
||||
startTimeStr := strings.TrimSpace(c.GetString("startTime"))
|
||||
endTimeStr := strings.TrimSpace(c.GetString("endTime"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemOperationLog)).Filter("delete_time__isnull", true)
|
||||
|
||||
// 条件拼装
|
||||
cond := orm.NewCondition()
|
||||
needCond := false
|
||||
|
||||
if module != "" {
|
||||
cond = cond.And("module", module)
|
||||
needCond = true
|
||||
}
|
||||
if action != "" {
|
||||
cond = cond.And("action", action)
|
||||
needCond = true
|
||||
}
|
||||
if statusStr != "" {
|
||||
if st, err := strconv.Atoi(statusStr); err == nil {
|
||||
cond = cond.And("status", st)
|
||||
needCond = true
|
||||
}
|
||||
}
|
||||
if keyword != "" {
|
||||
kw := orm.NewCondition().
|
||||
Or("module__icontains", keyword).
|
||||
Or("action__icontains", keyword).
|
||||
Or("method__icontains", keyword).
|
||||
Or("url__icontains", keyword).
|
||||
Or("ip__icontains", keyword).
|
||||
Or("user_agent__icontains", keyword)
|
||||
if uid, err := strconv.ParseUint(keyword, 10, 64); err == nil && uid > 0 {
|
||||
kw = kw.Or("user_id", uid)
|
||||
}
|
||||
cond = cond.AndCond(kw)
|
||||
needCond = true
|
||||
}
|
||||
if t, err := parseTimeFlexible(startTimeStr); err == nil && !t.IsZero() {
|
||||
cond = cond.And("create_time__gte", t)
|
||||
needCond = true
|
||||
}
|
||||
if t, err := parseTimeFlexible(endTimeStr); err == nil && !t.IsZero() {
|
||||
cond = cond.And("create_time__lte", t)
|
||||
needCond = true
|
||||
}
|
||||
|
||||
if needCond {
|
||||
qs = qs.SetCond(cond)
|
||||
}
|
||||
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取操作日志失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.SystemOperationLog
|
||||
_, 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 {
|
||||
item := map[string]interface{}{
|
||||
"id": rows[i].ID,
|
||||
"tid": rows[i].Tid,
|
||||
"user_id": rows[i].UserID,
|
||||
"module": rows[i].Module,
|
||||
"action": rows[i].Action,
|
||||
"method": rows[i].Method,
|
||||
"url": rows[i].URL,
|
||||
"ip": rows[i].IP,
|
||||
"user_agent": rows[i].UserAgent,
|
||||
"request_data": rows[i].RequestData,
|
||||
"response_data": rows[i].ResponseData,
|
||||
"status": rows[i].Status,
|
||||
"error_message": rows[i].ErrorMessage,
|
||||
"execution_time": rows[i].ExecutionTime,
|
||||
"create_time": rows[i].CreateTime.Format("2006-01-02 15:04:05"),
|
||||
"update_time": "",
|
||||
}
|
||||
if rows[i].UpdateTime != nil {
|
||||
item["update_time"] = rows[i].UpdateTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": list,
|
||||
"total": total,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Detail GET /backend/operationLogs/:id
|
||||
func (c *BackendOperationLogController) Detail() {
|
||||
if _, err := c.backendClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
var row models.SystemOperationLog
|
||||
err = models.Orm.QueryTable(new(models.SystemOperationLog)).
|
||||
Filter("id", id).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
c.jsonErr(404, 404, "记录不存在")
|
||||
return
|
||||
}
|
||||
out := map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"tid": row.Tid,
|
||||
"user_id": row.UserID,
|
||||
"module": row.Module,
|
||||
"action": row.Action,
|
||||
"method": row.Method,
|
||||
"url": row.URL,
|
||||
"ip": row.IP,
|
||||
"user_agent": row.UserAgent,
|
||||
"request_data": row.RequestData,
|
||||
"response_data": row.ResponseData,
|
||||
"status": row.Status,
|
||||
"error_message": row.ErrorMessage,
|
||||
"execution_time": row.ExecutionTime,
|
||||
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/operationLogs/:id
|
||||
func (c *BackendOperationLogController) Delete() {
|
||||
if _, err := c.backendClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
n, err := models.Orm.QueryTable(new(models.SystemOperationLog)).
|
||||
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.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type backendBatchDeletePayload struct {
|
||||
IDs []uint64 `json:"ids"`
|
||||
}
|
||||
|
||||
// BatchDelete POST /backend/operationLogs/batchDelete
|
||||
func (c *BackendOperationLogController) BatchDelete() {
|
||||
if _, err := c.backendClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p backendBatchDeletePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if len(p.IDs) == 0 {
|
||||
c.jsonErr(400, 400, "请选择要删除的日志")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.SystemOperationLog)).
|
||||
Filter("id__in", p.IDs).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": now})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "批量删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "批量删除成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Statistics GET /backend/operationLogs/statistics
|
||||
// 供前端筛选项:modules/actions
|
||||
func (c *BackendOperationLogController) Statistics() {
|
||||
if _, err := c.backendClaims(); err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var moduleRows []models.SystemOperationLog
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemOperationLog)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("module__isnull", false).
|
||||
Limit(1000).
|
||||
All(&moduleRows, "Module")
|
||||
modSet := map[string]struct{}{}
|
||||
for i := range moduleRows {
|
||||
m := strings.TrimSpace(moduleRows[i].Module)
|
||||
if m != "" {
|
||||
modSet[m] = struct{}{}
|
||||
}
|
||||
}
|
||||
modules := make([]string, 0, len(modSet))
|
||||
for k := range modSet {
|
||||
modules = append(modules, k)
|
||||
}
|
||||
|
||||
var actionRows []models.SystemOperationLog
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemOperationLog)).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("action__isnull", false).
|
||||
Limit(1000).
|
||||
All(&actionRows, "Action")
|
||||
actSet := map[string]struct{}{}
|
||||
for i := range actionRows {
|
||||
a := strings.TrimSpace(actionRows[i].Action)
|
||||
if a != "" {
|
||||
actSet[a] = struct{}{}
|
||||
}
|
||||
}
|
||||
actions := make([]string, 0, len(actSet))
|
||||
for k := range actSet {
|
||||
actions = append(actions, k)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"modules": modules,
|
||||
"actions": actions,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendSiteSettingsController 租户站点设置(站点基本信息)
|
||||
// 对应前端 normalSettings.vue 的:
|
||||
// - GET /backend/normalInfos
|
||||
// - POST /backend/saveNormalInfos
|
||||
// - GET /platform/normalInfos
|
||||
// - POST /platform/saveNormalInfos
|
||||
type BackendSiteSettingsController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *BackendSiteSettingsController) 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 *BackendSiteSettingsController) claimsByPath() (*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")
|
||||
}
|
||||
|
||||
path := strings.ToLower(c.Ctx.Request.URL.Path)
|
||||
if strings.HasPrefix(path, "/platform/") {
|
||||
if claims.UserType != "platform" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
} else if strings.HasPrefix(path, "/backend/") {
|
||||
if claims.UserType != "backend" {
|
||||
return nil, fmt.Errorf("无权访问")
|
||||
}
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func parseBackendUint64Flexible(v interface{}) uint64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
if x <= 0 {
|
||||
return 0
|
||||
}
|
||||
return uint64(x)
|
||||
case string:
|
||||
s := strings.TrimSpace(x)
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil || n == 0 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
type backendNormalInfosOutput struct {
|
||||
Sitename string `json:"sitename"`
|
||||
Companyintroduction string `json:"companyintroduction"`
|
||||
Description string `json:"description"`
|
||||
Copyright string `json:"copyright"`
|
||||
Companyname string `json:"companyname"`
|
||||
Icp string `json:"icp"`
|
||||
Logo string `json:"logo"`
|
||||
Logow string `json:"logow"`
|
||||
Ico string `json:"ico"`
|
||||
}
|
||||
|
||||
// GetNormalInfos GET /backend/normalInfos 或 /platform/normalInfos
|
||||
func (c *BackendSiteSettingsController) GetNormalInfos() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 优先使用 token 中的租户 id;若为 0,则允许前端通过查询参数传入(兼容历史/平台端)。
|
||||
tid := uint64(claims.TenantId)
|
||||
if tid == 0 {
|
||||
tidStr := strings.TrimSpace(c.GetString("tid"))
|
||||
if tidStr != "" {
|
||||
if n, err := strconv.ParseUint(tidStr, 10, 64); err == nil {
|
||||
tid = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out := backendNormalInfosOutput{
|
||||
Sitename: "",
|
||||
Companyintroduction: "",
|
||||
Description: "",
|
||||
Copyright: "",
|
||||
Companyname: "",
|
||||
Icp: "",
|
||||
Logo: "",
|
||||
Logow: "",
|
||||
Ico: "",
|
||||
}
|
||||
|
||||
// tid 缺失时不报错,直接返回空对象给前端渲染(避免 UI 直接崩)。
|
||||
if tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.TenantSiteSetting
|
||||
_, err = models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Limit(1).
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if len(rows) > 0 {
|
||||
r := rows[0]
|
||||
out.Sitename = r.Sitename
|
||||
out.Companyintroduction = r.Companyintroduction
|
||||
out.Logo = r.Logo
|
||||
out.Logow = r.Logow
|
||||
out.Ico = r.Ico
|
||||
out.Description = r.Description
|
||||
out.Copyright = r.Copyright
|
||||
out.Companyname = r.Companyname
|
||||
out.Icp = r.Icp
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type backendNormalInfosPayload struct {
|
||||
// 前端会传 tid(但我们仍优先使用 token 的 tenant_id)
|
||||
Tid interface{} `json:"tid"`
|
||||
|
||||
Sitename string `json:"sitename"`
|
||||
Companyintroduction string `json:"companyintroduction"`
|
||||
Logo string `json:"logo"`
|
||||
Logow string `json:"logow"`
|
||||
Ico string `json:"ico"`
|
||||
Description string `json:"description"`
|
||||
Copyright string `json:"copyright"`
|
||||
Companyname string `json:"companyname"`
|
||||
Icp string `json:"icp"`
|
||||
}
|
||||
|
||||
// SaveNormalInfos POST /backend/saveNormalInfos 或 /platform/saveNormalInfos
|
||||
func (c *BackendSiteSettingsController) SaveNormalInfos() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var p backendNormalInfosPayload
|
||||
if uerr := json.Unmarshal(raw, &p); uerr != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tid := uint64(claims.TenantId)
|
||||
if tid == 0 {
|
||||
tid = parseBackendUint64Flexible(p.Tid)
|
||||
}
|
||||
if tid == 0 {
|
||||
c.jsonErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
sitename := strings.TrimSpace(p.Sitename)
|
||||
if sitename == "" {
|
||||
c.jsonErr(400, 400, "站点名称不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
up := map[string]interface{}{
|
||||
"tid": tid,
|
||||
"sitename": sitename,
|
||||
"companyintroduction": strings.TrimSpace(p.Companyintroduction),
|
||||
"logo": strings.TrimSpace(p.Logo),
|
||||
"logow": strings.TrimSpace(p.Logow),
|
||||
"ico": strings.TrimSpace(p.Ico),
|
||||
"description": strings.TrimSpace(p.Description),
|
||||
"copyright": strings.TrimSpace(p.Copyright),
|
||||
"companyname": strings.TrimSpace(p.Companyname),
|
||||
"icp": strings.TrimSpace(p.Icp),
|
||||
"update_time": now,
|
||||
}
|
||||
|
||||
cnt, err := models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Count()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
row := &models.TenantSiteSetting{
|
||||
Tid: tid,
|
||||
Sitename: sitename,
|
||||
Companyintroduction: strings.TrimSpace(p.Companyintroduction),
|
||||
Logo: strings.TrimSpace(p.Logo),
|
||||
Logow: strings.TrimSpace(p.Logow),
|
||||
Ico: strings.TrimSpace(p.Ico),
|
||||
Description: strings.TrimSpace(p.Description),
|
||||
Copyright: strings.TrimSpace(p.Copyright),
|
||||
Companyname: strings.TrimSpace(p.Companyname),
|
||||
Icp: strings.TrimSpace(p.Icp),
|
||||
CreateTime: now,
|
||||
UpdateTime: &now,
|
||||
}
|
||||
_, err = models.Orm.Insert(row)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err = models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(up)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendSiteSettingsController) resolveBackendTenantID(claims *jwtutil.Claims, payloadTid interface{}) uint64 {
|
||||
tid := uint64(claims.TenantId)
|
||||
if tid == 0 {
|
||||
tid = parseBackendUint64Flexible(payloadTid)
|
||||
}
|
||||
if tid == 0 {
|
||||
tidStr := strings.TrimSpace(c.GetString("tid"))
|
||||
if tidStr != "" {
|
||||
if n, err := strconv.ParseUint(tidStr, 10, 64); err == nil {
|
||||
tid = n
|
||||
}
|
||||
}
|
||||
}
|
||||
return tid
|
||||
}
|
||||
|
||||
func (c *BackendSiteSettingsController) ensureBackendSettingItemsTable() error {
|
||||
_, err := models.Orm.Raw(`
|
||||
CREATE TABLE IF NOT EXISTS yz_system_tenant_setting_items (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
tid BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
setting_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||
setting_value LONGTEXT NULL,
|
||||
create_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
delete_time DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_tid_key (tid, setting_key),
|
||||
KEY idx_tid (tid),
|
||||
KEY idx_delete_time (delete_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户站点扩展设置';
|
||||
`).Exec()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *BackendSiteSettingsController) getBackendSettingItems(tid uint64, keys []string) (map[string]string, error) {
|
||||
out := make(map[string]string, len(keys))
|
||||
for _, key := range keys {
|
||||
out[key] = ""
|
||||
}
|
||||
|
||||
if err := c.ensureBackendSettingItemsTable(); err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
type rowItem struct {
|
||||
SettingKey string
|
||||
SettingValue string
|
||||
}
|
||||
var rows []rowItem
|
||||
_, err := models.Orm.Raw(
|
||||
"SELECT setting_key, IFNULL(setting_value, '') AS setting_value FROM yz_system_tenant_setting_items WHERE tid = ? AND setting_key IN ('"+strings.Join(keys, "','")+"') AND delete_time IS NULL",
|
||||
tid,
|
||||
).QueryRows(&rows)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
out[row.SettingKey] = row.SettingValue
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *BackendSiteSettingsController) saveBackendSettingItems(tid uint64, values map[string]string) error {
|
||||
if err := c.ensureBackendSettingItemsTable(); err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range values {
|
||||
_, err := models.Orm.Raw(`
|
||||
INSERT INTO yz_system_tenant_setting_items (tid, setting_key, setting_value, create_time, update_time)
|
||||
VALUES (?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), update_time = NOW(), delete_time = NULL
|
||||
`, tid, key, value).Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLegalInfos GET /backend/legalInfos
|
||||
func (c *BackendSiteSettingsController) GetLegalInfos() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tid := c.resolveBackendTenantID(claims, nil)
|
||||
if tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": []map[string]string{
|
||||
{"label": "legalNotice", "value": ""},
|
||||
{"label": "privacyTerms", "value": ""},
|
||||
}}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
values, err := c.getBackendSettingItems(tid, []string{"legalNotice", "privacyTerms"})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": []map[string]string{
|
||||
{"label": "legalNotice", "value": values["legalNotice"]},
|
||||
{"label": "privacyTerms", "value": values["privacyTerms"]},
|
||||
}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type backendLegalInfosPayload struct {
|
||||
Tid interface{} `json:"tid"`
|
||||
LegalNotice string `json:"legalNotice"`
|
||||
PrivacyTerms string `json:"privacyTerms"`
|
||||
}
|
||||
|
||||
// SaveLegalInfos POST /backend/saveLegalInfos
|
||||
func (c *BackendSiteSettingsController) SaveLegalInfos() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p backendLegalInfosPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tid := c.resolveBackendTenantID(claims, p.Tid)
|
||||
if tid == 0 {
|
||||
c.jsonErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err = c.saveBackendSettingItems(tid, map[string]string{
|
||||
"legalNotice": strings.TrimSpace(p.LegalNotice),
|
||||
"privacyTerms": strings.TrimSpace(p.PrivacyTerms),
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetCompanyInfos GET /backend/companyInfos
|
||||
func (c *BackendSiteSettingsController) GetCompanyInfos() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tid := c.resolveBackendTenantID(claims, nil)
|
||||
out := map[string]interface{}{
|
||||
"contact_phone": "",
|
||||
"contact_email": "",
|
||||
"address": "",
|
||||
"worktime": "",
|
||||
}
|
||||
if tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var row models.SystemTenant
|
||||
err = models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if row.ContactPhone != nil {
|
||||
out["contact_phone"] = *row.ContactPhone
|
||||
}
|
||||
if row.ContactEmail != nil {
|
||||
out["contact_email"] = *row.ContactEmail
|
||||
}
|
||||
if row.Address != nil {
|
||||
out["address"] = *row.Address
|
||||
}
|
||||
if row.Worktime != nil {
|
||||
out["worktime"] = *row.Worktime
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type backendCompanyInfosPayload struct {
|
||||
Tid interface{} `json:"tid"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
Address string `json:"address"`
|
||||
Worktime string `json:"worktime"`
|
||||
}
|
||||
|
||||
// SaveCompanyInfos POST /backend/saveCompanyInfos
|
||||
func (c *BackendSiteSettingsController) SaveCompanyInfos() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p backendCompanyInfosPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tid := c.resolveBackendTenantID(claims, p.Tid)
|
||||
if tid == 0 {
|
||||
c.jsonErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{
|
||||
"contact_phone": strings.TrimSpace(p.ContactPhone),
|
||||
"contact_email": strings.TrimSpace(p.ContactEmail),
|
||||
"address": strings.TrimSpace(p.Address),
|
||||
"worktime": strings.TrimSpace(p.Worktime),
|
||||
"update_time": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetCompanySeo GET /backend/companySeo
|
||||
func (c *BackendSiteSettingsController) GetCompanySeo() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tid := c.resolveBackendTenantID(claims, nil)
|
||||
out := map[string]string{
|
||||
"seoTitle": "",
|
||||
"seoKeywords": "",
|
||||
"seoDescription": "",
|
||||
}
|
||||
if tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
values, err := c.getBackendSettingItems(tid, []string{"seoTitle", "seoKeywords", "seoDescription"})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
out["seoTitle"] = values["seoTitle"]
|
||||
out["seoKeywords"] = values["seoKeywords"]
|
||||
out["seoDescription"] = values["seoDescription"]
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type backendCompanySeoPayload struct {
|
||||
Tid interface{} `json:"tid"`
|
||||
SeoTitle string `json:"seoTitle"`
|
||||
SeoKeywords string `json:"seoKeywords"`
|
||||
SeoDescription string `json:"seoDescription"`
|
||||
}
|
||||
|
||||
// SaveCompanySeo POST /backend/saveCompanySeo
|
||||
func (c *BackendSiteSettingsController) SaveCompanySeo() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p backendCompanySeoPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tid := c.resolveBackendTenantID(claims, p.Tid)
|
||||
if tid == 0 {
|
||||
c.jsonErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
err = c.saveBackendSettingItems(tid, map[string]string{
|
||||
"seoTitle": strings.TrimSpace(p.SeoTitle),
|
||||
"seoKeywords": strings.TrimSpace(p.SeoKeywords),
|
||||
"seoDescription": strings.TrimSpace(p.SeoDescription),
|
||||
})
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
func jsonErr(c *beego.Controller, httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type domainPoolPayload struct {
|
||||
ID uint64 `json:"id"`
|
||||
MainDomain string `json:"main_domain"`
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
var subDomainRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
@@ -16,6 +17,12 @@ type platformLoginRequest struct {
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
Code string `json:"code"`
|
||||
// 极验4验证参数
|
||||
CaptchaID string `json:"captcha_id"`
|
||||
LotNumber string `json:"lot_number"`
|
||||
PassToken string `json:"pass_token"`
|
||||
GenTime string `json:"gen_time"`
|
||||
CaptchaOutput string `json:"captcha_output"`
|
||||
}
|
||||
|
||||
type backendLoginRequest struct {
|
||||
@@ -34,9 +41,15 @@ type PlatformAuthController struct {
|
||||
func (c *PlatformAuthController) LoginPlatform() {
|
||||
var req platformLoginRequest
|
||||
|
||||
// 支持前端以 JSON body 方式提交
|
||||
body, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
// 先尝试从缓存读取
|
||||
body := c.Ctx.Input.RequestBody
|
||||
|
||||
// 如果缓存为空,直接从请求体读取
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
fmt.Println("读取请求体失败:", err)
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 400,
|
||||
"msg": "参数错误",
|
||||
@@ -44,17 +57,34 @@ func (c *PlatformAuthController) LoginPlatform() {
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
fmt.Println("请求体为空")
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 400,
|
||||
"msg": "参数错误",
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("登录请求体:", string(body))
|
||||
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
fmt.Println("JSON解析失败:", err, "body:", string(body))
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 400,
|
||||
"msg": "参数错误",
|
||||
"msg": "参数错误: " + err.Error(),
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("解析后的请求: %+v\n", req)
|
||||
|
||||
if req.Account == "" || req.Password == "" {
|
||||
fmt.Println("账号或密码为空, account:", req.Account, "password:", req.Password)
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 400,
|
||||
"msg": "用户名或密码不能为空",
|
||||
@@ -62,9 +92,27 @@ func (c *PlatformAuthController) LoginPlatform() {
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
cfg, _ := models.GetPlatformLoginVerify()
|
||||
if cfg.OpenVerifyEnabled == 1 {
|
||||
if cfg.VerifyType == "sms" || cfg.VerifyType == "email" {
|
||||
// 极验验证
|
||||
if cfg.VerifyType == "geetest4" {
|
||||
if req.LotNumber == "" || req.PassToken == "" || req.GenTime == "" || req.CaptchaOutput == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "请完成人机验证"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
// TODO: 这里应该调用极验服务端SDK验证,暂时跳过验证
|
||||
// 如果需要严格验证,需要集成极验服务端SDK
|
||||
} else if cfg.VerifyType == "geetest3" {
|
||||
// 极验3验证
|
||||
if req.CaptchaOutput == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "请完成人机验证"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
// TODO: 这里应该调用极验服务端SDK验证,暂时跳过验证
|
||||
} else if cfg.VerifyType == "sms" || cfg.VerifyType == "email" {
|
||||
if strings.TrimSpace(req.Code) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "请输入验证码"}
|
||||
_ = c.ServeJSON()
|
||||
@@ -111,8 +159,8 @@ func (c *PlatformAuthController) LoginPlatform() {
|
||||
func (c *PlatformAuthController) LoginBackend() {
|
||||
var req backendLoginRequest
|
||||
|
||||
body, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
@@ -222,7 +270,7 @@ func (c *PlatformAuthController) SendLoginCode() {
|
||||
TenantName string `json:"tenant_name"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||||
_ = c.ServeJSON()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -45,12 +44,6 @@ func requirePlatform(c *beego.Controller) (*jwtutil.Claims, error) {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func jsonErr(c *beego.Controller, httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ===== 主域名池 =====
|
||||
|
||||
// Index GET /platform/domain/pool/index?page=&pageSize=&main_domain=&status=
|
||||
@@ -150,12 +143,6 @@ func (c *PlatformDomainPoolController) GetEnabledDomains() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type domainPoolPayload struct {
|
||||
ID uint64 `json:"id"`
|
||||
MainDomain string `json:"main_domain"`
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
// Create POST /platform/domain/pool/create
|
||||
func (c *PlatformDomainPoolController) Create() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
@@ -397,8 +384,6 @@ func (c *PlatformTenantDomainController) MyDomains() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
var subDomainRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`)
|
||||
|
||||
// Apply POST /platform/domain/tenant/apply body:{tid,sub_domain,main_domain}
|
||||
func (c *PlatformTenantDomainController) Apply() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
@@ -23,10 +23,10 @@ type PlatformFileController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
const fileUploadMaxMB = 200
|
||||
const fileUploadMaxBytes = fileUploadMaxMB * 1024 * 1024
|
||||
const platformFileUploadMaxMB = 2048 // 2GB,适用于大型软件安装包
|
||||
const platformFileUploadMaxBytes = platformFileUploadMaxMB * 1024 * 1024
|
||||
|
||||
var fileTypeByCategory = map[string]uint8{
|
||||
var platformFileTypeByCategory = map[string]uint8{
|
||||
"image": 1,
|
||||
"document": 2,
|
||||
"video": 3,
|
||||
@@ -34,7 +34,7 @@ var fileTypeByCategory = map[string]uint8{
|
||||
"appsupgrade": 2,
|
||||
}
|
||||
|
||||
var allowedExtByCategory = map[string][]string{
|
||||
var platformAllowedExtByCategory = map[string][]string{
|
||||
"image": {"jpg", "jpeg", "png", "gif", "bmp", "webp"},
|
||||
"document": {"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt"},
|
||||
"video": {"mp4", "webm", "mov"},
|
||||
@@ -89,12 +89,12 @@ func (c *PlatformFileController) jsonOK(data interface{}) {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func detectFileType(ext string) uint8 {
|
||||
func platformDetectFileType(ext string) uint8 {
|
||||
ext = strings.ToLower(strings.TrimPrefix(ext, "."))
|
||||
for cat, exts := range allowedExtByCategory {
|
||||
for cat, exts := range platformAllowedExtByCategory {
|
||||
for _, e := range exts {
|
||||
if e == ext {
|
||||
if t, ok := fileTypeByCategory[cat]; ok {
|
||||
if t, ok := platformFileTypeByCategory[cat]; ok {
|
||||
return t
|
||||
}
|
||||
return 2
|
||||
@@ -104,7 +104,7 @@ func detectFileType(ext string) uint8 {
|
||||
return 2
|
||||
}
|
||||
|
||||
func fileExt(name string) string {
|
||||
func platformFileExt(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if i := strings.LastIndex(name, "."); i >= 0 && i < len(name)-1 {
|
||||
return strings.ToLower(name[i+1:])
|
||||
@@ -112,7 +112,7 @@ func fileExt(name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func fileToMap(f *models.SystemFile) map[string]interface{} {
|
||||
func platformFileToMap(f *models.SystemFile) map[string]interface{} {
|
||||
ct := f.CreateTime.Format("2006-01-02 15:04:05")
|
||||
m := map[string]interface{}{
|
||||
"id": f.ID,
|
||||
@@ -138,7 +138,7 @@ func fileToMap(f *models.SystemFile) map[string]interface{} {
|
||||
return m
|
||||
}
|
||||
|
||||
func removePhysicalBySrc(webSrc string) {
|
||||
func platformRemovePhysicalBySrc(webSrc string) {
|
||||
webSrc = strings.TrimSpace(webSrc)
|
||||
if webSrc == "" {
|
||||
return
|
||||
@@ -188,7 +188,7 @@ func (c *PlatformFileController) GetAllFiles() {
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for i := range rows {
|
||||
list = append(list, fileToMap(&rows[i]))
|
||||
list = append(list, platformFileToMap(&rows[i]))
|
||||
}
|
||||
c.jsonOK(map[string]interface{}{
|
||||
"list": list,
|
||||
@@ -234,7 +234,7 @@ func (c *PlatformFileController) GetUserCate() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type createCateBody struct {
|
||||
type platformCreateCateBody struct {
|
||||
Name string `json:"name"`
|
||||
Tuid *uint64 `json:"tuid"`
|
||||
}
|
||||
@@ -252,7 +252,7 @@ func (c *PlatformFileController) CreateFileCate() {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body createCateBody
|
||||
var body platformCreateCateBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
@@ -282,7 +282,7 @@ func (c *PlatformFileController) CreateFileCate() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type renameCateBody struct {
|
||||
type platformRenameCateBody struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ func (c *PlatformFileController) RenameFileCate() {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body renameCateBody
|
||||
var body platformRenameCateBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
@@ -421,7 +421,7 @@ func (c *PlatformFileController) GetCateFiles() {
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for i := range rows {
|
||||
list = append(list, fileToMap(&rows[i]))
|
||||
list = append(list, platformFileToMap(&rows[i]))
|
||||
}
|
||||
c.jsonOK(map[string]interface{}{
|
||||
"list": list,
|
||||
@@ -456,7 +456,7 @@ func (c *PlatformFileController) GetFileByID() {
|
||||
c.jsonErr(404, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
c.jsonOK(fileToMap(&f))
|
||||
c.jsonOK(platformFileToMap(&f))
|
||||
}
|
||||
|
||||
// UploadFile POST /platform/uploadfile
|
||||
@@ -467,7 +467,7 @@ func (c *PlatformFileController) UploadFile() {
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
if err := c.Ctx.Request.ParseMultipartForm(fileUploadMaxBytes); err != nil {
|
||||
if err := c.Ctx.Request.ParseMultipartForm(platformFileUploadMaxBytes); err != nil {
|
||||
c.jsonErr(400, 400, "解析上传失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
@@ -478,50 +478,40 @@ func (c *PlatformFileController) UploadFile() {
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
if header != nil && header.Size > fileUploadMaxBytes {
|
||||
c.jsonErr(400, 400, fmt.Sprintf("文件大小不能超过%dMB", fileUploadMaxMB))
|
||||
if header != nil && header.Size > platformFileUploadMaxBytes {
|
||||
c.jsonErr(400, 400, fmt.Sprintf("文件大小不能超过%dMB", platformFileUploadMaxMB))
|
||||
return
|
||||
}
|
||||
|
||||
ext := fileExt(header.Filename)
|
||||
ext := platformFileExt(header.Filename)
|
||||
if ext == "" {
|
||||
c.jsonErr(400, 400, "无法识别文件扩展名")
|
||||
return
|
||||
}
|
||||
|
||||
tmpPath := filepath.Join(os.TempDir(), fmt.Sprintf("up_%d_%s", time.Now().UnixNano(), header.Filename))
|
||||
tmp, err := os.Create(tmpPath)
|
||||
// 获取存储服务
|
||||
storageService, err := services.GetStorageService()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "创建临时文件失败")
|
||||
return
|
||||
}
|
||||
n, copyErr := io.Copy(tmp, fh)
|
||||
_ = tmp.Close()
|
||||
if copyErr != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
c.jsonErr(500, 500, "读取文件失败")
|
||||
return
|
||||
}
|
||||
if n > fileUploadMaxBytes {
|
||||
_ = os.Remove(tmpPath)
|
||||
c.jsonErr(400, 400, fmt.Sprintf("文件大小不能超过%dMB", fileUploadMaxMB))
|
||||
return
|
||||
}
|
||||
sum, err := md5HashFile(tmpPath)
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
c.jsonErr(500, 500, "计算文件摘要失败")
|
||||
c.jsonErr(500, 500, "获取存储服务失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
result, err := storageService.Upload(fh, header)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "上传文件失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否已存在(通过MD5)
|
||||
var exist models.SystemFile
|
||||
err = models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("md5", sum).
|
||||
Filter("md5", result.MD5).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&exist)
|
||||
if err == nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
// 文件已存在,返回已有记录
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 201,
|
||||
"msg": "文件已存在",
|
||||
@@ -535,23 +525,7 @@ func (c *PlatformFileController) UploadFile() {
|
||||
return
|
||||
}
|
||||
|
||||
datePath := time.Now().Format("2006/01/02")
|
||||
saveName := fmt.Sprintf("%s/%d.%s", datePath, time.Now().UnixNano(), ext)
|
||||
destDir := filepath.Join("uploads", filepath.FromSlash(datePath))
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
c.jsonErr(500, 500, "创建目录失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
destPath := filepath.Join("uploads", filepath.FromSlash(saveName))
|
||||
if err := os.Rename(tmpPath, destPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
c.jsonErr(500, 500, "保存文件失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
webURL := "/" + strings.ReplaceAll(filepath.ToSlash(destPath), "\\", "/")
|
||||
|
||||
// 获取分类
|
||||
cateStr := c.GetString("cate")
|
||||
var cate uint64
|
||||
if cateStr != "" {
|
||||
@@ -566,21 +540,23 @@ func (c *PlatformFileController) UploadFile() {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件记录到数据库
|
||||
row := &models.SystemFile{
|
||||
Tid: tid,
|
||||
Uid: &adminID,
|
||||
Tuid: tuidPtr,
|
||||
Name: header.Filename,
|
||||
Type: detectFileType(ext),
|
||||
Type: platformDetectFileType(ext),
|
||||
Cate: cate,
|
||||
Size: uint64(n),
|
||||
Src: webURL,
|
||||
Size: uint64(result.Size),
|
||||
Src: result.URL,
|
||||
Uploader: adminID,
|
||||
Md5: sum,
|
||||
Md5: result.MD5,
|
||||
}
|
||||
id, err := models.Orm.Insert(row)
|
||||
if err != nil {
|
||||
removePhysicalBySrc(webURL)
|
||||
// 数据库插入失败,尝试删除已上传的文件
|
||||
_ = storageService.Delete(result.Key)
|
||||
c.jsonErr(500, 500, "上传失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
@@ -589,7 +565,7 @@ func (c *PlatformFileController) UploadFile() {
|
||||
"code": 200,
|
||||
"msg": "上传成功",
|
||||
"data": map[string]interface{}{
|
||||
"url": webURL,
|
||||
"url": result.URL,
|
||||
"id": uint64(id),
|
||||
"name": header.Filename,
|
||||
},
|
||||
@@ -597,7 +573,7 @@ func (c *PlatformFileController) UploadFile() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func md5HashFile(path string) (string, error) {
|
||||
func platformMd5HashFile(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -610,7 +586,7 @@ func md5HashFile(path string) (string, error) {
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
type updateFileBody struct {
|
||||
type platformUpdateFileBody struct {
|
||||
Name *string `json:"name"`
|
||||
Cate *uint64 `json:"cate"`
|
||||
}
|
||||
@@ -634,7 +610,7 @@ func (c *PlatformFileController) UpdateFile() {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body updateFileBody
|
||||
var body platformUpdateFileBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
@@ -724,7 +700,7 @@ func (c *PlatformFileController) DeleteFilePermanently() {
|
||||
c.jsonErr(404, 404, "文件不存在")
|
||||
return
|
||||
}
|
||||
removePhysicalBySrc(f.Src)
|
||||
platformRemovePhysicalBySrc(f.Src)
|
||||
_, err = models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", id).
|
||||
Filter("tid", tid).
|
||||
@@ -770,7 +746,7 @@ func (c *PlatformFileController) MoveFile() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
type idsBody struct {
|
||||
type platformIdsBody struct {
|
||||
IDs []uint64 `json:"ids"`
|
||||
Cate *uint64 `json:"cate"`
|
||||
}
|
||||
@@ -788,7 +764,7 @@ func (c *PlatformFileController) BatchDeleteFiles() {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body idsBody
|
||||
var body platformIdsBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
@@ -805,7 +781,7 @@ func (c *PlatformFileController) BatchDeleteFiles() {
|
||||
Filter("tid", tid).
|
||||
One(&f)
|
||||
if e == nil && f.Src != "" {
|
||||
removePhysicalBySrc(f.Src)
|
||||
platformRemovePhysicalBySrc(f.Src)
|
||||
}
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
@@ -837,7 +813,7 @@ func (c *PlatformFileController) BatchDeleteFilesPermanently() {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body idsBody
|
||||
var body platformIdsBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
@@ -856,7 +832,7 @@ func (c *PlatformFileController) BatchDeleteFilesPermanently() {
|
||||
return
|
||||
}
|
||||
for i := range rows {
|
||||
removePhysicalBySrc(rows[i].Src)
|
||||
platformRemovePhysicalBySrc(rows[i].Src)
|
||||
}
|
||||
n, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id__in", body.IDs).
|
||||
@@ -899,7 +875,7 @@ func (c *PlatformFileController) BatchMoveFiles() {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var body idsBody
|
||||
var body platformIdsBody
|
||||
if err := json.Unmarshal(raw, &body); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformHomeController 平台首页统计(需登录)
|
||||
type PlatformHomeController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func cellToDateKey(v interface{}) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case []byte:
|
||||
s := strings.TrimSpace(string(x))
|
||||
if len(s) >= 10 {
|
||||
return s[:10]
|
||||
}
|
||||
return s
|
||||
case string:
|
||||
s := strings.TrimSpace(x)
|
||||
if len(s) >= 10 {
|
||||
return s[:10]
|
||||
}
|
||||
return s
|
||||
case time.Time:
|
||||
if x.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return x.In(time.Local).Format("2006-01-02")
|
||||
default:
|
||||
s := strings.TrimSpace(fmt.Sprint(x))
|
||||
if len(s) >= 10 {
|
||||
return s[:10]
|
||||
}
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func cellToInt64(v interface{}) int64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case []byte:
|
||||
n, _ := strconv.ParseInt(strings.TrimSpace(string(x)), 10, 64)
|
||||
return n
|
||||
case int64:
|
||||
return x
|
||||
case int32:
|
||||
return int64(x)
|
||||
case int:
|
||||
return int64(x)
|
||||
default:
|
||||
n, _ := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(x)), 10, 64)
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
func queryExtractedCountByDay(table string, start, endExclusive time.Time) (map[string]int64, error) {
|
||||
// 不按 delete_time 过滤:部分库未删除行存 0000-00-00 或非 NULL,会导致统计全空。
|
||||
// Raw + QueryRows 对别名映射不稳定,改用 Values 解析 d/c。
|
||||
sql := fmt.Sprintf(`
|
||||
SELECT DATE(extracted_time) AS d, COUNT(*) AS c
|
||||
FROM %s
|
||||
WHERE is_extracted IN (1, 2)
|
||||
AND extracted_time IS NOT NULL
|
||||
AND extracted_time >= ?
|
||||
AND extracted_time < ?
|
||||
GROUP BY DATE(extracted_time)
|
||||
ORDER BY d
|
||||
`, table)
|
||||
var maps []orm.Params
|
||||
_, err := models.Orm.Raw(sql, start, endExclusive).Values(&maps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(map[string]int64, len(maps))
|
||||
for _, m := range maps {
|
||||
var dk, ck interface{}
|
||||
for _, k := range []string{"d", "D"} {
|
||||
if v, ok := m[k]; ok {
|
||||
dk = v
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, k := range []string{"c", "C"} {
|
||||
if v, ok := m[k]; ok {
|
||||
ck = v
|
||||
break
|
||||
}
|
||||
}
|
||||
key := cellToDateKey(dk)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
out[key] = cellToInt64(ck)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AccountPoolDailyExtract GET /platform/home/accountPoolDailyExtract?days=14
|
||||
// 按天统计各号池「已提取」数量,依据 extracted_time 落在当天的记录。
|
||||
func (c *PlatformHomeController) AccountPoolDailyExtract() {
|
||||
if _, err := requirePlatformAuth(&c.Controller); err != nil {
|
||||
poolJSONErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
n, _ := c.GetInt("days", 14)
|
||||
if n < 1 {
|
||||
n = 1
|
||||
}
|
||||
if n > 90 {
|
||||
n = 90
|
||||
}
|
||||
|
||||
now := time.Now().In(time.Local)
|
||||
today0 := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
firstDay := today0.AddDate(0, 0, -(n - 1))
|
||||
endExclusive := today0.AddDate(0, 0, 1)
|
||||
|
||||
cursorTable := (&models.PlatformAccountPoolCursor{}).TableName()
|
||||
windsurfTable := (&models.PlatformAccountPoolWindsurf{}).TableName()
|
||||
kiroTable := (&models.PlatformAccountPoolKiro{}).TableName()
|
||||
|
||||
mCursor, err := queryExtractedCountByDay(cursorTable, firstDay, endExclusive)
|
||||
if err != nil {
|
||||
poolJSONErr(&c.Controller, 500, 500, "统计 Cursor 失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
mWindsurf, err := queryExtractedCountByDay(windsurfTable, firstDay, endExclusive)
|
||||
if err != nil {
|
||||
poolJSONErr(&c.Controller, 500, 500, "统计 Windsurf 失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
mKiro, err := queryExtractedCountByDay(kiroTable, firstDay, endExclusive)
|
||||
if err != nil {
|
||||
poolJSONErr(&c.Controller, 500, 500, "统计 Kiro 失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dayKeys := make([]string, 0, n)
|
||||
dayLabels := make([]string, 0, n)
|
||||
cursorVals := make([]int64, 0, n)
|
||||
windsurfVals := make([]int64, 0, n)
|
||||
kiroVals := make([]int64, 0, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
d := firstDay.AddDate(0, 0, i)
|
||||
key := d.Format("2006-01-02")
|
||||
dayKeys = append(dayKeys, key)
|
||||
dayLabels = append(dayLabels, d.Format("01/02"))
|
||||
cursorVals = append(cursorVals, mCursor[key])
|
||||
windsurfVals = append(windsurfVals, mWindsurf[key])
|
||||
kiroVals = append(kiroVals, mKiro[key])
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"days": dayLabels,
|
||||
"dayKeys": dayKeys,
|
||||
"cursor": int64SliceToInt(cursorVals),
|
||||
"windsurf": int64SliceToInt(windsurfVals),
|
||||
"kiro": int64SliceToInt(kiroVals),
|
||||
"daysLength": n,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func int64SliceToInt(in []int64) []int {
|
||||
out := make([]int, len(in))
|
||||
for i, v := range in {
|
||||
out[i] = int(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func countPoolInventory(mi interface{}, soldOnly bool) (int64, error) {
|
||||
qs := models.Orm.QueryTable(mi).Filter("delete_time__isnull", true)
|
||||
if soldOnly {
|
||||
qs = qs.Filter("is_extracted__in", 1, 2)
|
||||
}
|
||||
n, err := qs.Count()
|
||||
return n, err
|
||||
}
|
||||
|
||||
// AccountPoolInventoryTotals GET /platform/home/accountPoolInventoryTotals
|
||||
// 各号池:账号总数(未删)、已售卖(is_extracted 为 1 或 2)
|
||||
func (c *PlatformHomeController) AccountPoolInventoryTotals() {
|
||||
if _, err := requirePlatformAuth(&c.Controller); err != nil {
|
||||
poolJSONErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
type invModule struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Total int64 `json:"total"`
|
||||
Sold int64 `json:"sold"`
|
||||
}
|
||||
|
||||
modules := []invModule{
|
||||
{Key: "cursor", Label: "Cursor"},
|
||||
{Key: "krio", Label: "Kiro"},
|
||||
{Key: "windsurf", Label: "Windsurf"},
|
||||
}
|
||||
modelsList := []interface{}{
|
||||
new(models.PlatformAccountPoolCursor),
|
||||
new(models.PlatformAccountPoolKiro),
|
||||
new(models.PlatformAccountPoolWindsurf),
|
||||
}
|
||||
|
||||
var grandTotal, grandSold int64
|
||||
for i := range modules {
|
||||
tot, err := countPoolInventory(modelsList[i], false)
|
||||
if err != nil {
|
||||
poolJSONErr(&c.Controller, 500, 500, "统计失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
sd, err := countPoolInventory(modelsList[i], true)
|
||||
if err != nil {
|
||||
poolJSONErr(&c.Controller, 500, 500, "统计失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
modules[i].Total = tot
|
||||
modules[i].Sold = sd
|
||||
grandTotal += tot
|
||||
grandSold += sd
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"modules": modules,
|
||||
"grandTotal": grandTotal,
|
||||
"grandSold": grandSold,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -14,23 +14,23 @@ import (
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// SiteSettingsController 租户站点设置(站点基本信息)
|
||||
// PlatformSiteSettingsController 租户站点设置(站点基本信息)
|
||||
// 对应前端 normalSettings.vue 的:
|
||||
// - GET /backend/normalInfos
|
||||
// - POST /backend/saveNormalInfos
|
||||
// - GET /platform/normalInfos
|
||||
// - POST /platform/saveNormalInfos
|
||||
type SiteSettingsController struct {
|
||||
type PlatformSiteSettingsController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *SiteSettingsController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
func (c *PlatformSiteSettingsController) 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 *SiteSettingsController) claimsByPath() (*jwtutil.Claims, error) {
|
||||
func (c *PlatformSiteSettingsController) claimsByPath() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
@@ -96,7 +96,7 @@ type normalInfosOutput struct {
|
||||
}
|
||||
|
||||
// GetNormalInfos GET /backend/normalInfos 或 /platform/normalInfos
|
||||
func (c *SiteSettingsController) GetNormalInfos() {
|
||||
func (c *PlatformSiteSettingsController) GetNormalInfos() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
@@ -176,7 +176,7 @@ type normalInfosPayload struct {
|
||||
}
|
||||
|
||||
// SaveNormalInfos POST /backend/saveNormalInfos 或 /platform/saveNormalInfos
|
||||
func (c *SiteSettingsController) SaveNormalInfos() {
|
||||
func (c *PlatformSiteSettingsController) SaveNormalInfos() {
|
||||
claims, err := c.claimsByPath()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
@@ -269,4 +269,3 @@ func (c *SiteSettingsController) SaveNormalInfos() {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformTenantController 平台端租户管理
|
||||
// PlatformTenantController ĺšłĺ°çŤŻç§ćˇçŽĄç?
|
||||
type PlatformTenantController struct {
|
||||
beego.Controller
|
||||
}
|
||||
@@ -33,27 +33,38 @@ type tenantDTO struct {
|
||||
DeleteTime *time.Time `json:"delete_time,omitempty"`
|
||||
}
|
||||
|
||||
func toTenantDTO(t models.Tenant) tenantDTO {
|
||||
func stringValue(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func toTenantDTO(t models.SystemTenant) tenantDTO {
|
||||
ct := t.CreateTime
|
||||
ut := t.UpdateTime
|
||||
return tenantDTO{
|
||||
ID: t.ID,
|
||||
TenantCode: t.TenantCode,
|
||||
TenantName: t.TenantName,
|
||||
ContactPerson: t.ContactPerson,
|
||||
ContactPhone: t.ContactPhone,
|
||||
ContactEmail: t.ContactEmail,
|
||||
Address: t.Address,
|
||||
Worktime: t.Worktime,
|
||||
ContactPerson: stringValue(t.ContactPerson),
|
||||
ContactPhone: stringValue(t.ContactPhone),
|
||||
ContactEmail: stringValue(t.ContactEmail),
|
||||
Address: stringValue(t.Address),
|
||||
Worktime: stringValue(t.Worktime),
|
||||
Status: t.Status,
|
||||
Remark: t.Remark,
|
||||
Remark: stringValue(t.Remark),
|
||||
CreateTime: &ct,
|
||||
UpdateTime: &ut,
|
||||
DeleteTime: t.DeleteTime,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTenant 获取租户列表
|
||||
// GetTenant čˇĺç§ćˇĺ襨
|
||||
// GET /platform/tenant/getTenant?page=1&pageSize=10&tenant_name=...&tenant_code=...&contact_person=...&contact_phone=...
|
||||
func (c *PlatformTenantController) GetTenant() {
|
||||
page, _ := c.GetInt("page", 1)
|
||||
@@ -70,7 +81,7 @@ func (c *PlatformTenantController) GetTenant() {
|
||||
contactPerson := strings.TrimSpace(c.GetString("contact_person"))
|
||||
contactPhone := strings.TrimSpace(c.GetString("contact_phone"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.Tenant))
|
||||
qs := models.Orm.QueryTable(new(models.SystemTenant))
|
||||
if tenantName != "" {
|
||||
qs = qs.Filter("tenant_name__icontains", tenantName)
|
||||
}
|
||||
@@ -86,15 +97,15 @@ func (c *PlatformTenantController) GetTenant() {
|
||||
|
||||
total, err := qs.Count()
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取租户失败: " + err.Error()}
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "čˇĺç§ćˇĺ¤ąč´Ľ: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.Tenant
|
||||
var rows []models.SystemTenant
|
||||
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取租户失败: " + err.Error()}
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "čˇĺç§ćˇĺ¤ąč´Ľ: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
@@ -115,18 +126,18 @@ func (c *PlatformTenantController) GetTenant() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantDetail 获取租户详情
|
||||
// GetTenantDetail čˇĺç§ćˇčŻŚć
|
||||
// GET /platform/tenant/getTenantDetail/:id
|
||||
func (c *PlatformTenantController) GetTenantDetail() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "ć ćID"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var t models.Tenant
|
||||
err = models.Orm.QueryTable(new(models.Tenant)).Filter("id", id).One(&t)
|
||||
var t models.SystemTenant
|
||||
err = models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", id).One(&t)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "租户不存在"}
|
||||
_ = c.ServeJSON()
|
||||
@@ -154,7 +165,7 @@ type tenantPayload struct {
|
||||
}
|
||||
|
||||
func (c *PlatformTenantController) parseTenantPayload() (tenantPayload, error) {
|
||||
// 优先从表单读取(createTenant 使用 multipart/form-data)
|
||||
// äźĺ
äťčĄ¨ĺ话ĺďźcreateTenant ä˝żç¨ multipart/form-dataďź?
|
||||
p := tenantPayload{
|
||||
TenantCode: strings.TrimSpace(c.GetString("tenant_code")),
|
||||
TenantName: strings.TrimSpace(c.GetString("tenant_name")),
|
||||
@@ -172,7 +183,7 @@ func (c *PlatformTenantController) parseTenantPayload() (tenantPayload, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果关键字段为空,尝试从 JSON body 解析(editTenant 默认 JSON)
|
||||
// ĺŚćĺ
łéŽĺ掾为犺ďźĺ°čŻäť JSON body č§ŁćďźeditTenant éťčޤ JSONďź?
|
||||
if p.TenantName == "" && p.TenantCode == "" {
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if len(raw) > 0 {
|
||||
@@ -182,25 +193,25 @@ func (c *PlatformTenantController) parseTenantPayload() (tenantPayload, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// CreateTenant 创建租户
|
||||
// CreateTenant ĺĺťşç§ćˇ
|
||||
// POST /platform/tenant/createTenant
|
||||
func (c *PlatformTenantController) CreateTenant() {
|
||||
p, _ := c.parseTenantPayload()
|
||||
if strings.TrimSpace(p.TenantName) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户名称不能为空"}
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "ç§ćˇĺç§°ä¸č˝ä¸şçŠş"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.TenantCode) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户编码不能为空"}
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "ç§ćˇçźç ä¸č˝ä¸şçŠş"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 校验编码唯一
|
||||
cnt, err := models.Orm.QueryTable(new(models.Tenant)).Filter("tenant_code", p.TenantCode).Count()
|
||||
// ć ĄéŞçźç ĺŻä¸
|
||||
cnt, err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_code", p.TenantCode).Count()
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "ĺ坺夹贼: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
@@ -215,21 +226,21 @@ func (c *PlatformTenantController) CreateTenant() {
|
||||
status = *p.Status
|
||||
}
|
||||
|
||||
t := models.Tenant{
|
||||
t := models.SystemTenant{
|
||||
TenantCode: p.TenantCode,
|
||||
TenantName: p.TenantName,
|
||||
ContactPerson: p.ContactPerson,
|
||||
ContactPhone: p.ContactPhone,
|
||||
ContactEmail: p.ContactEmail,
|
||||
Address: p.Address,
|
||||
Worktime: p.Worktime,
|
||||
ContactPerson: stringPtr(p.ContactPerson),
|
||||
ContactPhone: stringPtr(p.ContactPhone),
|
||||
ContactEmail: stringPtr(p.ContactEmail),
|
||||
Address: stringPtr(p.Address),
|
||||
Worktime: stringPtr(p.Worktime),
|
||||
Status: status,
|
||||
Remark: p.Remark,
|
||||
Remark: stringPtr(p.Remark),
|
||||
}
|
||||
|
||||
id, err := models.Orm.Insert(&t)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "ĺ坺夹贼: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
@@ -242,19 +253,19 @@ func (c *PlatformTenantController) CreateTenant() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// EditTenant 编辑租户
|
||||
// EditTenant çźčžç§ćˇ
|
||||
// POST /platform/tenant/editTenant/:id
|
||||
func (c *PlatformTenantController) EditTenant() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "ć ćID"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
p, _ := c.parseTenantPayload()
|
||||
if strings.TrimSpace(p.TenantName) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户名称不能为空"}
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "ç§ćˇĺç§°ä¸č˝ä¸şçŠş"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
@@ -272,9 +283,9 @@ func (c *PlatformTenantController) EditTenant() {
|
||||
update["status"] = *p.Status
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.Tenant)).Filter("id", id).Update(update)
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", id).Update(update)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "ć´ć°ĺ¤ąč´Ľ: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
@@ -283,19 +294,19 @@ func (c *PlatformTenantController) EditTenant() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteTenant 删除租户
|
||||
// DeleteTenant ĺ é¤ç§ćˇ
|
||||
// DELETE /platform/tenant/deleteTenant/:id
|
||||
func (c *PlatformTenantController) DeleteTenant() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "ć ćID"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.Tenant)).Filter("id", id).Delete()
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenant)).Filter("id", id).Delete()
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败: " + err.Error()}
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "ĺ é¤ĺ¤ąč´Ľ: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
@@ -304,20 +315,20 @@ func (c *PlatformTenantController) DeleteTenant() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// FindTenantCode 校验租户编码是否重复
|
||||
// FindTenantCode ć ĄéŞç§ćˇçźç ćŻĺŚéĺ¤
|
||||
// GET /platform/tenant/findTenantCode?tenant_code=xxxxxx
|
||||
// 返回 code=200 表示可用;非200表示重复/不可用(前端会自动重新生成)
|
||||
// čżĺ code=200 襨示ĺŻç¨ďźé200襨示éĺ¤/ä¸ĺŻç¨ďźĺ獯äźčŞĺ¨éć°çćďź
|
||||
func (c *PlatformTenantController) FindTenantCode() {
|
||||
code := strings.TrimSpace(c.GetString("tenant_code"))
|
||||
if code == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tenant_code 不能为空"}
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tenant_code ä¸č˝ä¸şçŠş"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
cnt, err := models.Orm.QueryTable(new(models.Tenant)).Filter("tenant_code", code).Count()
|
||||
cnt, err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_code", code).Count()
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "校验失败: " + err.Error()}
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "ć ĄéŞĺ¤ąč´Ľ: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
@@ -330,4 +341,3 @@ func (c *PlatformTenantController) FindTenantCode() {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformTenantUserController 平台端租户-用户绑定管理
|
||||
// PlatformTenantUserController 平台租户用户绑定管理
|
||||
type PlatformTenantUserController struct {
|
||||
beego.Controller
|
||||
}
|
||||
@@ -35,14 +35,14 @@ type tenantUserPayload struct {
|
||||
Remark *string `json:"remark"`
|
||||
}
|
||||
|
||||
// GetTenantUserList 获取绑定列表(支持按 tid / uid 过滤;keyword 对姓名/手机/邮箱/账号模糊 OR 匹配)
|
||||
// GET /platform/tenantUser/list?tid=1&uid=2&keyword=张
|
||||
// GetTenantUserList 获取绑定列表(支持按 tid / uid 过滤,keyword 对姓名/手机/邮箱/账号模糊匹配)
|
||||
// GET /platform/tenantUser/list?tid=1&uid=2&keyword=xxx
|
||||
func (c *PlatformTenantUserController) GetTenantUserList() {
|
||||
tid, _ := c.GetUint64("tid")
|
||||
uid, _ := c.GetUint64("uid")
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.TenantUser))
|
||||
qs := models.Orm.QueryTable(new(models.SystemTenantUser))
|
||||
|
||||
var cond *orm.Condition
|
||||
needCond := false
|
||||
@@ -77,7 +77,7 @@ func (c *PlatformTenantUserController) GetTenantUserList() {
|
||||
qs = qs.SetCond(cond)
|
||||
}
|
||||
|
||||
var rows []models.TenantUser
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := qs.OrderBy("-is_default", "-id").All(&rows)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}
|
||||
@@ -96,7 +96,7 @@ func (c *PlatformTenantUserController) GetTenantUserList() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantUsersByTid 兼容路径参数方式获取租户用户列表
|
||||
// GetTenantUsersByTid 兼容旧路由,根据租户 ID 获取租户用户列表
|
||||
// GET /platform/getTenantUsers/:tid
|
||||
func (c *PlatformTenantUserController) GetTenantUsersByTid() {
|
||||
tidStr := c.Ctx.Input.Param(":tid")
|
||||
@@ -106,8 +106,8 @@ func (c *PlatformTenantUserController) GetTenantUsersByTid() {
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
var rows []models.TenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.TenantUser)).
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
@@ -134,8 +134,8 @@ func (c *PlatformTenantUserController) GetTenantUserDetail() {
|
||||
return
|
||||
}
|
||||
|
||||
var row models.TenantUser
|
||||
err = models.Orm.QueryTable(new(models.TenantUser)).Filter("id", id).One(&row)
|
||||
var row models.SystemTenantUser
|
||||
err = models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).One(&row)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "记录不存在"}
|
||||
_ = c.ServeJSON()
|
||||
@@ -146,7 +146,7 @@ func (c *PlatformTenantUserController) GetTenantUserDetail() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// CreateTenantUser 创建租户用户绑定(写入表 yz_system_tenant_user;uid 为空时由 generateTenantUID 生成)
|
||||
// CreateTenantUser 创建租户用户绑定(写入 yz_system_tenant_user;uid 为空时自动生成)
|
||||
// POST /platform/tenantUser/create
|
||||
func (c *PlatformTenantUserController) CreateTenantUser() {
|
||||
p, ok := c.parsePayload()
|
||||
@@ -194,7 +194,7 @@ func (c *PlatformTenantUserController) CreateTenantUser() {
|
||||
status = *p.Status
|
||||
}
|
||||
|
||||
id, err := services.BindTenantUser(p.Tid, p.Uid, p.Account, p.Name, p.Phone, p.Email, p.Password, isDefault, status, p.Remark)
|
||||
id, err := services.BindTenantUser(p.Tid, p.Uid, p.Account, p.Name, p.Phone, p.Email, nil, nil, p.Password, isDefault, status, p.Remark)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
@@ -267,7 +267,7 @@ func (c *PlatformTenantUserController) EditTenantUser() {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.TenantUser)).Filter("id", id).Update(update)
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).Update(update)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
@@ -317,7 +317,7 @@ func generateTenantUID(tid uint64) (uint64, error) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
for i := 0; i < 8; i++ {
|
||||
uid := uint64(10000000 + rand.Intn(90000000))
|
||||
cnt, err := models.Orm.QueryTable(new(models.TenantUser)).
|
||||
cnt, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("uid", uid).
|
||||
Count()
|
||||
|
||||
@@ -88,7 +88,7 @@ func (c *PlatformUserController) AddUser() {
|
||||
hashedPwd := hashed
|
||||
password := &hashedPwd
|
||||
|
||||
_, err := services.BindTenantUser(p.Tid, uid, account, name, phone, email, password, 0, status, p.Remark)
|
||||
_, err := services.BindTenantUser(p.Tid, uid, account, name, phone, email, nil, nil, password, 0, status, p.Remark)
|
||||
if err == nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
||||
"github.com/qiniu/go-sdk/v7/storage"
|
||||
)
|
||||
|
||||
// QiniuUploadController 七牛云上传控制器
|
||||
type QiniuUploadController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// platformClaims 获取平台端 JWT claims
|
||||
func (c *QiniuUploadController) platformClaims() (*jwtutil.Claims, error) {
|
||||
auth := c.Ctx.Request.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
parts := strings.Split(auth, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil, fmt.Errorf("token 格式错误")
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token 无效")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// effectiveTid 获取有效的租户 ID
|
||||
func (c *QiniuUploadController) effectiveTid(claims *jwtutil.Claims) uint64 {
|
||||
if claims.TenantId > 0 {
|
||||
return uint64(claims.TenantId)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// jsonErr 返回错误响应
|
||||
func (c *QiniuUploadController) jsonErr(httpStatus, bizCode int, msg string) {
|
||||
c.Ctx.Output.SetStatus(httpStatus)
|
||||
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// jsonOK 返回成功响应
|
||||
func (c *QiniuUploadController) jsonOK(data interface{}) {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ParseJSON 解析 JSON 请求体
|
||||
func (c *QiniuUploadController) ParseJSON(v interface{}) error {
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
return fmt.Errorf("请求体为空")
|
||||
}
|
||||
return json.Unmarshal(body, v)
|
||||
}
|
||||
|
||||
// GetUploadToken 获取上传凭证
|
||||
// GET /platform/qiniu/token
|
||||
func (c *QiniuUploadController) GetUploadToken() {
|
||||
_, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取存储配置
|
||||
cfg, err := models.GetStorageConfig()
|
||||
if err != nil || cfg.StorageType != "qiniu" {
|
||||
c.jsonErr(400, 400, "当前未配置七牛云存储")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查配置完整性
|
||||
if cfg.QiniuAccessKey == "" || cfg.QiniuSecretKey == "" || cfg.QiniuBucket == "" {
|
||||
c.jsonErr(500, 500, "七牛云配置不完整")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成文件 key(前端可以覆盖)
|
||||
datePath := time.Now().Format("2006/01/02")
|
||||
timestamp := time.Now().UnixNano()
|
||||
keyPrefix := fmt.Sprintf("%s/%d", datePath, timestamp)
|
||||
|
||||
// 创建上传策略
|
||||
mac := qbox.NewMac(cfg.QiniuAccessKey, cfg.QiniuSecretKey)
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: cfg.QiniuBucket,
|
||||
ReturnBody: `{"key":"$(key)","hash":"$(etag)","size":$(fsize),"mimeType":"$(mimeType)"}`,
|
||||
Expires: 3600, // 1小时有效期
|
||||
}
|
||||
upToken := putPolicy.UploadToken(mac)
|
||||
|
||||
// 返回上传凭证和配置
|
||||
c.jsonOK(map[string]interface{}{
|
||||
"token": upToken,
|
||||
"domain": cfg.QiniuDomain,
|
||||
"bucket": cfg.QiniuBucket,
|
||||
"region": cfg.QiniuRegion,
|
||||
"keyPrefix": keyPrefix,
|
||||
"expires": time.Now().Add(time.Hour).Unix(),
|
||||
"uploadUrl": getQiniuUploadURL(cfg.QiniuRegion),
|
||||
})
|
||||
}
|
||||
|
||||
// SaveFileRecord 保存文件记录
|
||||
// POST /platform/qiniu/save
|
||||
func (c *QiniuUploadController) SaveFileRecord() {
|
||||
claims, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := c.effectiveTid(claims)
|
||||
|
||||
// 调试:打印请求体
|
||||
body := c.Ctx.Input.RequestBody
|
||||
fmt.Println("SaveFileRecord 请求体长度:", len(body))
|
||||
fmt.Println("SaveFileRecord 请求体内容:", string(body))
|
||||
|
||||
// 解析请求参数
|
||||
type SaveRequest struct {
|
||||
Key string `json:"key"` // 七牛云文件 key
|
||||
Hash string `json:"hash"` // 文件 hash (etag)
|
||||
Size int64 `json:"size"` // 文件大小
|
||||
Name string `json:"name"` // 原始文件名
|
||||
MimeType string `json:"mimeType"` // 文件类型
|
||||
Cate uint64 `json:"cate"` // 分类 ID
|
||||
}
|
||||
|
||||
var req SaveRequest
|
||||
if err := c.ParseJSON(&req); err != nil {
|
||||
c.jsonErr(400, 400, "参数解析失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.Key == "" || req.Name == "" {
|
||||
c.jsonErr(400, 400, "缺少必填参数")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取存储配置
|
||||
cfg, err := models.GetStorageConfig()
|
||||
if err != nil || cfg.StorageType != "qiniu" {
|
||||
c.jsonErr(400, 400, "当前未配置七牛云存储")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建完整 URL
|
||||
domain := strings.TrimRight(cfg.QiniuDomain, "/")
|
||||
fileURL := fmt.Sprintf("%s/%s", domain, req.Key)
|
||||
|
||||
// 计算 MD5(使用 hash 作为 MD5,或者重新计算)
|
||||
md5Sum := req.Hash
|
||||
if md5Sum == "" {
|
||||
// 如果没有 hash,使用 key 生成一个唯一标识
|
||||
h := md5.New()
|
||||
h.Write([]byte(req.Key))
|
||||
md5Sum = hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// 检查文件是否已存在(通过 MD5)
|
||||
var exist models.SystemFile
|
||||
err = models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("md5", md5Sum).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&exist)
|
||||
if err == nil {
|
||||
// 文件已存在,返回已有记录
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 201,
|
||||
"msg": "文件已存在",
|
||||
"data": map[string]interface{}{
|
||||
"url": exist.Src,
|
||||
"id": exist.ID,
|
||||
"name": exist.Name,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 检测文件类型
|
||||
ext := getQiniuFileExt(req.Name)
|
||||
fileType := detectQiniuFileType(ext)
|
||||
|
||||
// 保存文件记录
|
||||
adminID := uint64(claims.UserID)
|
||||
row := &models.SystemFile{
|
||||
Tid: tid,
|
||||
Uid: &adminID,
|
||||
Name: req.Name,
|
||||
Type: fileType,
|
||||
Cate: req.Cate,
|
||||
Size: uint64(req.Size),
|
||||
Src: fileURL,
|
||||
Uploader: adminID,
|
||||
Md5: md5Sum,
|
||||
}
|
||||
|
||||
id, err := models.Orm.Insert(row)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "保存文件记录失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.jsonOK(map[string]interface{}{
|
||||
"url": fileURL,
|
||||
"id": uint64(id),
|
||||
"name": req.Name,
|
||||
"key": req.Key,
|
||||
})
|
||||
}
|
||||
|
||||
// GetStorageConfig 获取存储配置(前端用于判断上传方式)
|
||||
// GET /platform/storage/config
|
||||
func (c *QiniuUploadController) GetStorageConfig() {
|
||||
_, err := c.platformClaims()
|
||||
if err != nil {
|
||||
c.jsonErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := models.GetStorageConfig()
|
||||
if err != nil {
|
||||
c.jsonOK(map[string]interface{}{
|
||||
"storageType": "local",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 只返回必要的配置信息,不返回密钥
|
||||
c.jsonOK(map[string]interface{}{
|
||||
"storageType": cfg.StorageType,
|
||||
"qiniuDomain": cfg.QiniuDomain,
|
||||
"qiniuRegion": cfg.QiniuRegion,
|
||||
})
|
||||
}
|
||||
|
||||
// getQiniuUploadURL 根据区域获取上传地址
|
||||
func getQiniuUploadURL(region string) string {
|
||||
switch region {
|
||||
case "z0":
|
||||
return "https://up-z0.qiniup.com"
|
||||
case "z1":
|
||||
return "https://up-z1.qiniup.com"
|
||||
case "z2":
|
||||
return "https://up-z2.qiniup.com"
|
||||
case "na0":
|
||||
return "https://up-na0.qiniup.com"
|
||||
case "as0":
|
||||
return "https://up-as0.qiniup.com"
|
||||
case "cn-east-2":
|
||||
return "https://up-cn-east-2.qiniup.com"
|
||||
default:
|
||||
return "https://up-z0.qiniup.com" // 默认华东
|
||||
}
|
||||
}
|
||||
|
||||
// getQiniuFileExt 获取文件扩展名
|
||||
func getQiniuFileExt(filename string) string {
|
||||
parts := strings.Split(filename, ".")
|
||||
if len(parts) > 1 {
|
||||
return strings.ToLower(parts[len(parts)-1])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// detectQiniuFileType 检测文件类型
|
||||
func detectQiniuFileType(ext string) uint8 {
|
||||
imageExts := map[string]bool{
|
||||
"jpg": true, "jpeg": true, "png": true, "gif": true, "bmp": true,
|
||||
"webp": true, "svg": true, "ico": true,
|
||||
}
|
||||
videoExts := map[string]bool{
|
||||
"mp4": true, "avi": true, "mov": true, "wmv": true, "flv": true,
|
||||
"mkv": true, "webm": true, "m4v": true,
|
||||
}
|
||||
audioExts := map[string]bool{
|
||||
"mp3": true, "wav": true, "flac": true, "aac": true, "ogg": true,
|
||||
"m4a": true, "wma": true,
|
||||
}
|
||||
docExts := map[string]bool{
|
||||
"doc": true, "docx": true, "xls": true, "xlsx": true, "ppt": true,
|
||||
"pptx": true, "pdf": true, "txt": true, "md": true,
|
||||
}
|
||||
archiveExts := map[string]bool{
|
||||
"zip": true, "rar": true, "7z": true, "tar": true, "gz": true,
|
||||
"bz2": true, "xz": true,
|
||||
}
|
||||
executableExts := map[string]bool{
|
||||
"exe": true, "msi": true, "dmg": true, "pkg": true, "deb": true,
|
||||
"rpm": true, "apk": true, "msix": true,
|
||||
}
|
||||
|
||||
if imageExts[ext] {
|
||||
return 1 // 图片
|
||||
}
|
||||
if videoExts[ext] {
|
||||
return 2 // 视频
|
||||
}
|
||||
if audioExts[ext] {
|
||||
return 3 // 音频
|
||||
}
|
||||
if docExts[ext] {
|
||||
return 4 // 文档
|
||||
}
|
||||
if archiveExts[ext] || executableExts[ext] {
|
||||
return 5 // 压缩包/安装包
|
||||
}
|
||||
return 0 // 其他
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type StorageConfigController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type storageConfigPayload struct {
|
||||
StorageType string `json:"storage_type"`
|
||||
QiniuAccessKey *string `json:"qiniu_access_key"`
|
||||
QiniuSecretKey *string `json:"qiniu_secret_key"`
|
||||
QiniuBucket *string `json:"qiniu_bucket"`
|
||||
QiniuDomain *string `json:"qiniu_domain"`
|
||||
QiniuRegion *string `json:"qiniu_region"`
|
||||
}
|
||||
|
||||
func normalizeStorageType(v string) string {
|
||||
switch strings.TrimSpace(v) {
|
||||
case "local", "qiniu":
|
||||
return strings.TrimSpace(v)
|
||||
default:
|
||||
return "local"
|
||||
}
|
||||
}
|
||||
|
||||
// GetStorageConfig 获取存储配置
|
||||
// GET /platform/storageConfig
|
||||
func (c *StorageConfigController) GetStorageConfig() {
|
||||
cfg, err := models.GetStorageConfig()
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取配置失败"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"storage_type": cfg.StorageType,
|
||||
"qiniu_access_key": cfg.QiniuAccessKey,
|
||||
"qiniu_secret_key": cfg.QiniuSecretKey,
|
||||
"qiniu_bucket": cfg.QiniuBucket,
|
||||
"qiniu_domain": cfg.QiniuDomain,
|
||||
"qiniu_region": cfg.QiniuRegion,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// SaveStorageConfig 保存存储配置
|
||||
// POST /platform/saveStorageConfig
|
||||
func (c *StorageConfigController) SaveStorageConfig() {
|
||||
var p storageConfigPayload
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
storageType := normalizeStorageType(p.StorageType)
|
||||
|
||||
// 如果选择七牛云,验证必填字段
|
||||
if storageType == "qiniu" {
|
||||
if p.QiniuAccessKey == nil || strings.TrimSpace(*p.QiniuAccessKey) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "七牛云 AccessKey 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if p.QiniuSecretKey == nil || strings.TrimSpace(*p.QiniuSecretKey) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "七牛云 SecretKey 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if p.QiniuBucket == nil || strings.TrimSpace(*p.QiniuBucket) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "七牛云 Bucket 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if p.QiniuDomain == nil || strings.TrimSpace(*p.QiniuDomain) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "七牛云域名不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var existed models.StorageConfig
|
||||
err := models.Orm.QueryTable(new(models.StorageConfig)).OrderBy("-id").One(&existed)
|
||||
if err == nil {
|
||||
// 更新现有配置
|
||||
update := map[string]interface{}{
|
||||
"storage_type": storageType,
|
||||
"qiniu_access_key": p.QiniuAccessKey,
|
||||
"qiniu_secret_key": p.QiniuSecretKey,
|
||||
"qiniu_bucket": p.QiniuBucket,
|
||||
"qiniu_domain": p.QiniuDomain,
|
||||
"qiniu_region": p.QiniuRegion,
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.StorageConfig)).Filter("id", existed.ID).Update(update)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "保存失败"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 创建新配置
|
||||
row := &models.StorageConfig{
|
||||
StorageType: storageType,
|
||||
QiniuAccessKey: getStringValue(p.QiniuAccessKey),
|
||||
QiniuSecretKey: getStringValue(p.QiniuSecretKey),
|
||||
QiniuBucket: getStringValue(p.QiniuBucket),
|
||||
QiniuDomain: getStringValue(p.QiniuDomain),
|
||||
QiniuRegion: getStringValue(p.QiniuRegion),
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "保存失败"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func getStringValue(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type StorageMigrationController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// MigrateToQiniu 迁移文件到七牛云
|
||||
// POST /platform/storage/migrateToQiniu
|
||||
func (c *StorageMigrationController) MigrateToQiniu() {
|
||||
// 这里简化处理,实际应该使用异步任务
|
||||
// 可以使用 goroutine + 进度查询接口实现
|
||||
|
||||
// 获取租户ID(从token或参数)
|
||||
tid := uint64(1) // 示例,实际应从认证信息获取
|
||||
|
||||
progress, err := services.MigrateLocalToQiniu(tid)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 500,
|
||||
"msg": "迁移失败: " + err.Error(),
|
||||
"data": progress,
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "迁移完成",
|
||||
"data": map[string]interface{}{
|
||||
"total": progress.Total,
|
||||
"success": progress.Success,
|
||||
"failed": progress.Failed,
|
||||
"errors": progress.Errors,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetMigrationProgress 获取迁移进度
|
||||
// GET /platform/storage/migrationProgress
|
||||
func (c *StorageMigrationController) GetMigrationProgress() {
|
||||
// 这里需要实现进度查询逻辑
|
||||
// 可以使用全局变量或Redis存储进度信息
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"total": 0,
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
"current": "",
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# Go后端项目文档
|
||||
|
||||
## 📚 文档目录
|
||||
|
||||
### 开发文档
|
||||
- [后端开发规则](./后端开发规则.md)
|
||||
- [接口文件](./接口文件.md)
|
||||
- [服务端启动命令](./服务端启动命令.md)
|
||||
- [大文件上传配置](./大文件上传配置.md) - 文件上传限制和超时配置
|
||||
|
||||
### 存储配置功能文档
|
||||
- [📖 快速开始](./QUICK_START.md) - 5分钟快速上手
|
||||
- [📘 完整实现说明](./README_STORAGE.md) - 功能概述和使用指南
|
||||
- [📗 详细使用指南](./storage-config-guide.md) - 深入的配置和使用说明
|
||||
- [✅ 部署检查清单](./DEPLOYMENT_CHECKLIST.md) - 生产环境部署指南
|
||||
- [🎉 实现报告](./IMPLEMENTATION_COMPLETE.md) - 完整的实现细节
|
||||
|
||||
### 数据库文档
|
||||
- [SQL迁移脚本](./sql/) - 数据库迁移文件
|
||||
|
||||
## 🚀 快速导航
|
||||
|
||||
### 新手入门
|
||||
1. 阅读 [快速开始](./QUICK_START.md)
|
||||
2. 查看 [服务端启动命令](./服务端启动命令.md)
|
||||
3. 了解 [后端开发规则](./后端开发规则.md)
|
||||
|
||||
### 存储功能使用
|
||||
1. [快速开始](./QUICK_START.md) - 快速配置存储
|
||||
2. [完整实现说明](./README_STORAGE.md) - 了解核心功能
|
||||
3. [详细使用指南](./storage-config-guide.md) - 深入学习
|
||||
|
||||
### 生产部署
|
||||
1. [部署检查清单](./DEPLOYMENT_CHECKLIST.md) - 按清单逐项检查
|
||||
2. [实现报告](./IMPLEMENTATION_COMPLETE.md) - 了解技术细节
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
```
|
||||
go/
|
||||
├── controllers/ # 控制器层
|
||||
├── models/ # 数据模型层
|
||||
├── services/ # 业务服务层
|
||||
├── routers/ # 路由配置
|
||||
├── pkg/ # 公共包
|
||||
├── conf/ # 配置文件
|
||||
├── migrations/ # 数据库迁移
|
||||
├── scripts/ # 脚本工具
|
||||
└── docs/ # 文档(本目录)
|
||||
```
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [Beego框架文档](https://beego.vip/)
|
||||
- [七牛云开发文档](https://developer.qiniu.com/)
|
||||
- [Go语言官方文档](https://golang.org/doc/)
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### 2026-04-09
|
||||
- ✅ 增加大文件上传支持(最大 2GB)
|
||||
- ✅ 移除服务器超时限制
|
||||
- ✅ 优化 CORS 配置
|
||||
- ✅ 完善文件上传文档
|
||||
|
||||
### 2024-01-01
|
||||
- ✅ 完成存储配置功能
|
||||
- ✅ 支持本地存储和七牛云存储
|
||||
- ✅ 实现文件迁移功能
|
||||
- ✅ 完善文档体系
|
||||
@@ -0,0 +1,18 @@
|
||||
-- 创建存储配置表
|
||||
CREATE TABLE IF NOT EXISTS `yz_system_storage_config` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`storage_type` varchar(20) NOT NULL DEFAULT 'local' COMMENT '存储类型: local-本地存储, qiniu-七牛云',
|
||||
`qiniu_access_key` varchar(255) DEFAULT NULL COMMENT '七牛云AccessKey',
|
||||
`qiniu_secret_key` varchar(255) DEFAULT NULL COMMENT '七牛云SecretKey',
|
||||
`qiniu_bucket` varchar(128) DEFAULT NULL COMMENT '七牛云Bucket名称',
|
||||
`qiniu_domain` varchar(255) DEFAULT NULL COMMENT '七牛云CDN域名',
|
||||
`qiniu_region` varchar(50) DEFAULT NULL COMMENT '七牛云存储区域',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统存储配置表';
|
||||
|
||||
-- 插入默认配置(本地存储)
|
||||
INSERT INTO `yz_system_storage_config` (`storage_type`, `create_time`)
|
||||
VALUES ('local', NOW())
|
||||
ON DUPLICATE KEY UPDATE `storage_type` = 'local';
|
||||
@@ -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续杯激活码';
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
# 大文件上传配置说明
|
||||
|
||||
## 概述
|
||||
|
||||
为支持大型软件安装包(如桌面客户端安装程序)的上传,系统已调整文件上传限制和超时配置。
|
||||
|
||||
## 配置修改
|
||||
|
||||
### 1. 文件大小限制
|
||||
|
||||
**文件位置**: `go/controllers/platform_file.go`
|
||||
|
||||
**修改内容**:
|
||||
```go
|
||||
// 修改前
|
||||
const fileUploadMaxMB = 200 // 200MB
|
||||
|
||||
// 修改后
|
||||
const fileUploadMaxMB = 2048 // 2GB,适用于大型软件安装包
|
||||
```
|
||||
|
||||
### 2. 服务器超时配置
|
||||
|
||||
**文件位置**: `go/conf/app.conf`
|
||||
|
||||
**新增配置**:
|
||||
```ini
|
||||
# 服务器超时配置(支持大文件上传)
|
||||
# 0 表示不设置超时限制
|
||||
ServerTimeOut = 0
|
||||
# 最大请求体大小(字节),0 表示不限制
|
||||
MaxMemory = 0
|
||||
```
|
||||
|
||||
## CORS 配置
|
||||
|
||||
**文件位置**: `go/routers/router.go`
|
||||
|
||||
当前 CORS 配置允许跨域请求:
|
||||
|
||||
```go
|
||||
beego.InsertFilter("*", beego.BeforeRouter, func(ctx *context.Context) {
|
||||
ctx.Output.Header("Access-Control-Allow-Origin", "*")
|
||||
ctx.Output.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
|
||||
ctx.Output.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
|
||||
ctx.Output.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
if ctx.Input.Method() == "OPTIONS" {
|
||||
ctx.Output.Status = 200
|
||||
ctx.Output.Body([]byte(""))
|
||||
return
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 生产环境 CORS 配置建议
|
||||
|
||||
在生产环境中,建议将 `Access-Control-Allow-Origin` 设置为具体的前端域名:
|
||||
|
||||
```go
|
||||
// 开发环境
|
||||
ctx.Output.Header("Access-Control-Allow-Origin", "*")
|
||||
|
||||
// 生产环境(推荐)
|
||||
allowedOrigins := []string{
|
||||
"https://platform.yunzer.cn",
|
||||
"https://www.yunzer.cn",
|
||||
}
|
||||
origin := ctx.Request.Header.Get("Origin")
|
||||
for _, allowed := range allowedOrigins {
|
||||
if origin == allowed {
|
||||
ctx.Output.Header("Access-Control-Allow-Origin", origin)
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 上传流程
|
||||
|
||||
### 1. 文件上传接口
|
||||
|
||||
**路由**: `POST /platform/uploadfile`
|
||||
|
||||
**控制器**: `PlatformFileController.UploadFile`
|
||||
|
||||
**处理流程**:
|
||||
1. 验证用户身份(JWT token)
|
||||
2. 解析 multipart form(最大 2GB)
|
||||
3. 检查文件大小(不超过 2GB)
|
||||
4. 获取存储服务(本地或七牛云)
|
||||
5. 上传文件到存储服务
|
||||
6. 检查文件 MD5 是否已存在
|
||||
7. 保存文件记录到数据库
|
||||
8. 返回文件信息(URL、ID、名称)
|
||||
|
||||
### 2. 存储服务
|
||||
|
||||
系统支持两种存储方式:
|
||||
|
||||
- **本地存储**: 文件保存在 `uploads/` 目录
|
||||
- **七牛云存储**: 文件上传到七牛云 OSS
|
||||
|
||||
存储方式通过 `yz_system_storage_config` 表配置。
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. Nginx 反向代理配置
|
||||
|
||||
如果使用 Nginx 作为反向代理,需要调整以下配置:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.yunzer.cn;
|
||||
|
||||
# 客户端请求体大小限制(0 表示不限制)
|
||||
client_max_body_size 0;
|
||||
|
||||
# 客户端请求体缓冲区大小
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
# 超时配置
|
||||
client_body_timeout 3600s;
|
||||
send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_read_timeout 3600s;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8081;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 禁用请求体缓冲(直接流式传输)
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 磁盘空间监控
|
||||
|
||||
大文件上传需要足够的磁盘空间:
|
||||
|
||||
```bash
|
||||
# 检查磁盘空间
|
||||
df -h
|
||||
|
||||
# 监控 uploads 目录大小
|
||||
du -sh uploads/
|
||||
|
||||
# 设置磁盘空间告警(推荐使用监控工具)
|
||||
```
|
||||
|
||||
### 3. 数据库优化
|
||||
|
||||
对于频繁的文件查询,建议添加索引:
|
||||
|
||||
```sql
|
||||
-- MD5 索引(用于去重)
|
||||
CREATE INDEX idx_system_file_md5 ON yz_system_file(md5);
|
||||
|
||||
-- 租户 + 删除时间索引(用于文件列表查询)
|
||||
CREATE INDEX idx_system_file_tid_delete ON yz_system_file(tid, delete_time);
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 1. 上传失败:文件过大
|
||||
|
||||
**错误信息**: "文件大小不能超过 2048MB"
|
||||
|
||||
**解决方案**:
|
||||
- 检查 `fileUploadMaxMB` 常量设置
|
||||
- 确认 Nginx `client_max_body_size` 配置
|
||||
- 检查磁盘剩余空间
|
||||
|
||||
### 2. 上传超时
|
||||
|
||||
**错误信息**: "请求失败,请检查网络连接"
|
||||
|
||||
**解决方案**:
|
||||
- 检查 `app.conf` 中的 `ServerTimeOut` 配置
|
||||
- 检查 Nginx 超时配置
|
||||
- 检查网络带宽和稳定性
|
||||
|
||||
### 3. CORS 错误
|
||||
|
||||
**错误信息**: "已拦截跨源请求:同源策略禁止读取..."
|
||||
|
||||
**解决方案**:
|
||||
- 检查 `go/routers/router.go` 中的 CORS 配置
|
||||
- 确认 `Access-Control-Allow-Origin` 包含前端域名
|
||||
- 检查 `Access-Control-Allow-Headers` 包含 `Authorization`
|
||||
|
||||
### 4. 文件不存在(404)
|
||||
|
||||
**错误信息**: "请求的资源不存在"
|
||||
|
||||
**可能原因**:
|
||||
- 文件记录在数据库中不存在
|
||||
- 租户 ID (tid) 不匹配
|
||||
- 文件已被标记为删除
|
||||
|
||||
**解决方案**:
|
||||
```sql
|
||||
-- 检查文件记录
|
||||
SELECT * FROM yz_system_file WHERE id = 320;
|
||||
|
||||
-- 检查是否被删除
|
||||
SELECT * FROM yz_system_file WHERE id = 320 AND delete_time IS NULL;
|
||||
```
|
||||
|
||||
## 监控指标
|
||||
|
||||
建议监控以下指标:
|
||||
|
||||
1. **上传成功率**: 成功上传数 / 总上传请求数
|
||||
2. **平均上传时间**: 按文件大小分段统计
|
||||
3. **磁盘使用率**: uploads 目录大小 / 总磁盘空间
|
||||
4. **错误率**: 按错误类型分类统计
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `go/controllers/platform_file.go` - 文件上传控制器
|
||||
- `go/services/storage_service.go` - 存储服务接口
|
||||
- `go/conf/app.conf` - 服务器配置
|
||||
- `go/routers/router.go` - 路由和 CORS 配置
|
||||
- `go/models/system_file.go` - 文件数据模型
|
||||
|
||||
## 更新日志
|
||||
|
||||
- **2026-04-09**:
|
||||
- 文件大小限制从 200MB 提升到 2GB
|
||||
- 移除服务器超时限制
|
||||
- 更新文档
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Beego 文档 - 文件上传](https://beego.vip/docs/mvc/controller/file.md)
|
||||
- [Nginx 文件上传配置](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size)
|
||||
- [七牛云 Go SDK](https://developer.qiniu.com/kodo/1238/go)
|
||||
@@ -1,26 +0,0 @@
|
||||
## 接口文件
|
||||
|
||||
> 约定:每新增一个对外接口,都需要在本文件登记(端/方法/路径/描述/鉴权/入出参简述)。
|
||||
|
||||
### platform(平台端)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
|---|---|---|
|
||||
| `POST` | `/platform/login` | 平台登录 |
|
||||
| `POST` | `/platform/sendLoginCode` | 发送登录验证码 |
|
||||
| `POST` | `/platform/loginBySms` | 手机号验证码登录 |
|
||||
| `POST` | `/platform/logout` | 平台退出登录 |
|
||||
| `GET` | `/platform/login/getGeetest3Infos` | 获取极验3.0配置 |
|
||||
| `GET` | `/platform/login/getGeetest4Infos` | 获取极验4.0配置 |
|
||||
| `GET` | `/platform/login/getOpenVerify` | 判断是否开启登录验证 |
|
||||
| `POST` | `/platform/resetPassword` | 忘记密码重置 |
|
||||
| `POST` | `/platform/sendResetCode` | 发送找回密码验证码 |
|
||||
|
||||
#### `/platform/login` 详情
|
||||
|
||||
- 入参(JSON body):`{ "username": string, "password": string }`
|
||||
- 出参(JSON):`{ "success": boolean, "token": string }`
|
||||
- 说明:当前使用占位登录逻辑,仅校验非空并返回平台用户 JWT,后续接真实用户/租户表
|
||||
|
||||
> 其余 `/platform/*` 登录相关接口(发送验证码、极验、重置密码等)目前仅返回 `501 Not Implemented`,后续按实际需求逐步补全。
|
||||
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
# 文档整理说明
|
||||
|
||||
## 📁 文档结构
|
||||
|
||||
所有文档已按照项目结构整理到对应的 `docs/` 目录中。
|
||||
|
||||
### 后端文档 (go/docs/)
|
||||
|
||||
```
|
||||
go/docs/
|
||||
├── README.md # 文档索引(新增)
|
||||
├── 后端开发规则.md # 开发规范
|
||||
├── 接口文件.md # 接口文档
|
||||
├── 服务端启动命令.md # 启动说明
|
||||
├── QUICK_START.md # 快速开始(新增)
|
||||
├── README_STORAGE.md # 存储功能说明(新增)
|
||||
├── storage-config-guide.md # 存储详细指南(新增)
|
||||
├── DEPLOYMENT_CHECKLIST.md # 部署清单(新增)
|
||||
├── IMPLEMENTATION_COMPLETE.md # 实现报告(新增)
|
||||
├── 文档整理说明.md # 本文件(新增)
|
||||
└── sql/
|
||||
└── add_storage_config_table.sql # 数据库迁移
|
||||
```
|
||||
|
||||
### 前端文档 (platform/docs/)
|
||||
|
||||
```
|
||||
platform/docs/
|
||||
├── README.md # 文档索引(新增)
|
||||
├── dictionary-usage.md # 字典使用
|
||||
├── pinia-dict-guide.md # Pinia字典指南
|
||||
├── 一键复制.md # 复制功能
|
||||
├── 拼接接口路径.md # 接口路径
|
||||
├── 接口调用.md # 接口调用
|
||||
├── 获取缓存数据.md # 缓存数据
|
||||
├── 调用图片上传组件.md # 图片上传
|
||||
└── 调用字典.md # 字典调用
|
||||
```
|
||||
|
||||
### 项目根目录
|
||||
|
||||
```
|
||||
项目根目录/
|
||||
└── README.md # 总导航(新增)
|
||||
```
|
||||
|
||||
## 📝 文档分类
|
||||
|
||||
### 1. 开发文档
|
||||
- 后端开发规则.md
|
||||
- 接口文件.md
|
||||
- 服务端启动命令.md
|
||||
|
||||
### 2. 功能文档
|
||||
- dictionary-usage.md
|
||||
- pinia-dict-guide.md
|
||||
- 调用字典.md
|
||||
- 调用图片上传组件.md
|
||||
- 等...
|
||||
|
||||
### 3. 存储配置功能文档(新增)
|
||||
- QUICK_START.md - 快速开始
|
||||
- README_STORAGE.md - 功能说明
|
||||
- storage-config-guide.md - 详细指南
|
||||
- DEPLOYMENT_CHECKLIST.md - 部署清单
|
||||
- IMPLEMENTATION_COMPLETE.md - 实现报告
|
||||
|
||||
### 4. 索引文档(新增)
|
||||
- 项目根目录/README.md - 总导航
|
||||
- go/docs/README.md - 后端文档索引
|
||||
- platform/docs/README.md - 前端文档索引
|
||||
|
||||
## 🔍 文档查找
|
||||
|
||||
### 按功能查找
|
||||
|
||||
**存储配置功能**:
|
||||
1. 快速开始 → `go/docs/QUICK_START.md`
|
||||
2. 功能说明 → `go/docs/README_STORAGE.md`
|
||||
3. 详细指南 → `go/docs/storage-config-guide.md`
|
||||
4. 部署清单 → `go/docs/DEPLOYMENT_CHECKLIST.md`
|
||||
|
||||
**字典功能**:
|
||||
1. 使用说明 → `platform/docs/dictionary-usage.md`
|
||||
2. Pinia指南 → `platform/docs/pinia-dict-guide.md`
|
||||
|
||||
**图片上传**:
|
||||
1. 组件调用 → `platform/docs/调用图片上传组件.md`
|
||||
|
||||
### 按角色查找
|
||||
|
||||
**新手开发者**:
|
||||
1. 项目总览 → `README.md`
|
||||
2. 后端开发 → `go/docs/后端开发规则.md`
|
||||
3. 快速开始 → `go/docs/QUICK_START.md`
|
||||
|
||||
**运维人员**:
|
||||
1. 启动命令 → `go/docs/服务端启动命令.md`
|
||||
2. 部署清单 → `go/docs/DEPLOYMENT_CHECKLIST.md`
|
||||
|
||||
**产品经理**:
|
||||
1. 功能说明 → `go/docs/README_STORAGE.md`
|
||||
2. 实现报告 → `go/docs/IMPLEMENTATION_COMPLETE.md`
|
||||
|
||||
## 📋 文档规范
|
||||
|
||||
### 文件命名
|
||||
- 中文文档:使用中文名称(如:后端开发规则.md)
|
||||
- 英文文档:使用大写+下划线(如:README_STORAGE.md)
|
||||
- 索引文档:统一使用 README.md
|
||||
|
||||
### 文档结构
|
||||
```markdown
|
||||
# 标题
|
||||
|
||||
## 概述
|
||||
简要说明文档内容
|
||||
|
||||
## 目录
|
||||
- 章节1
|
||||
- 章节2
|
||||
|
||||
## 详细内容
|
||||
...
|
||||
|
||||
## 相关链接
|
||||
- 链接1
|
||||
- 链接2
|
||||
```
|
||||
|
||||
### 文档位置
|
||||
- 后端相关文档 → `go/docs/`
|
||||
- 前端相关文档 → `platform/docs/`
|
||||
- 移动端相关文档 → `babyhealth/docs/`
|
||||
- 项目总览 → 根目录 `README.md`
|
||||
|
||||
## 🔄 文档更新
|
||||
|
||||
### 新增文档
|
||||
1. 确定文档类型(后端/前端/通用)
|
||||
2. 放入对应的 `docs/` 目录
|
||||
3. 更新对应的 `README.md` 索引
|
||||
4. 如需要,更新根目录 `README.md`
|
||||
|
||||
### 修改文档
|
||||
1. 直接修改对应文档
|
||||
2. 更新文档底部的"最后更新"时间
|
||||
3. 如有重大变更,更新索引文档
|
||||
|
||||
### 删除文档
|
||||
1. 删除文档文件
|
||||
2. 从索引中移除引用
|
||||
3. 检查其他文档中的链接
|
||||
|
||||
## ✅ 整理完成清单
|
||||
|
||||
- [x] 创建后端文档索引 (go/docs/README.md)
|
||||
- [x] 创建前端文档索引 (platform/docs/README.md)
|
||||
- [x] 创建项目总导航 (README.md)
|
||||
- [x] 移动存储功能文档到 go/docs/
|
||||
- [x] 删除根目录的临时文档
|
||||
- [x] 创建文档整理说明(本文件)
|
||||
|
||||
## 📌 注意事项
|
||||
|
||||
1. **文档位置**: 所有文档必须放在对应项目的 `docs/` 目录中
|
||||
2. **索引更新**: 新增文档后必须更新索引文件
|
||||
3. **链接检查**: 修改文档位置后检查所有引用链接
|
||||
4. **命名规范**: 遵循统一的文件命名规范
|
||||
5. **内容质量**: 保持文档的准确性和时效性
|
||||
|
||||
## 🎯 后续优化
|
||||
|
||||
- [ ] 添加文档搜索功能
|
||||
- [ ] 生成文档网站(如使用 VuePress)
|
||||
- [ ] 添加文档版本管理
|
||||
- [ ] 自动化文档检查工具
|
||||
- [ ] 文档贡献指南
|
||||
|
||||
---
|
||||
|
||||
**整理完成时间**: 2024-01-01
|
||||
**整理人员**: AI Assistant
|
||||
+289
-12
@@ -1,23 +1,300 @@
|
||||
启动
|
||||
systemctl daemon-reload
|
||||
## 方式一:使用 systemd 服务(推荐)
|
||||
|
||||
### 自动安装(推荐)
|
||||
|
||||
使用安装脚本自动配置 systemd 服务:
|
||||
|
||||
```bash
|
||||
# 进入脚本目录
|
||||
cd /www/wwwroot/api.yunzer.cn/scripts
|
||||
|
||||
# 添加执行权限
|
||||
chmod +x install-systemd-service.sh
|
||||
|
||||
# 运行安装脚本
|
||||
sudo bash install-systemd-service.sh
|
||||
|
||||
或者
|
||||
|
||||
sudo env PATH=$PATH:/usr/local/btgo/bin bash install-systemd-service.sh
|
||||
|
||||
|
||||
```
|
||||
|
||||
脚本会自动:
|
||||
- 停止现有服务和进程
|
||||
- 创建正确的 systemd 配置文件
|
||||
- 启动服务
|
||||
- 启用开机自启
|
||||
- 显示服务状态和日志
|
||||
|
||||
### 手动安装
|
||||
|
||||
如果需要手动配置:
|
||||
|
||||
```bash
|
||||
# 1. 停止现有服务
|
||||
systemctl stop go-api
|
||||
pkill -f "go run main.go"
|
||||
|
||||
# 2. 复制服务文件
|
||||
sudo cp /www/wwwroot/api.yunzer.cn/scripts/go-api.service /etc/systemd/system/
|
||||
|
||||
# 3. 重载 systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# 4. 启动服务
|
||||
sudo systemctl start go-api
|
||||
|
||||
# 5. 启用开机自启
|
||||
sudo systemctl enable go-api
|
||||
|
||||
# 6. 查看状态
|
||||
sudo systemctl status go-api
|
||||
```
|
||||
|
||||
### 启动服务
|
||||
```bash
|
||||
systemctl start go-api
|
||||
```
|
||||
|
||||
### 查看状态
|
||||
```bash
|
||||
systemctl status go-api
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
```bash
|
||||
# 启动
|
||||
systemctl start go-api
|
||||
|
||||
查看是否成功
|
||||
# 停止
|
||||
systemctl stop go-api
|
||||
|
||||
# 重启
|
||||
systemctl restart go-api
|
||||
|
||||
# 查看状态
|
||||
systemctl status go-api
|
||||
|
||||
启动:systemctl start go-api
|
||||
停止:systemctl stop go-api
|
||||
重启:systemctl restart go-api
|
||||
查看状态:systemctl status go-api
|
||||
# 查看日志(systemd 日志)
|
||||
journalctl -u go-api -f
|
||||
|
||||
# 查看日志(文件日志)
|
||||
tail -f /www/wwwroot/api.yunzer.cn/go.log
|
||||
|
||||
后台直接启动
|
||||
# 开机自启
|
||||
systemctl enable go-api
|
||||
|
||||
# 禁用开机自启
|
||||
systemctl disable go-api
|
||||
```
|
||||
|
||||
## 方式二:使用管理脚本(推荐)
|
||||
|
||||
### 脚本位置
|
||||
```bash
|
||||
/www/wwwroot/api.yunzer.cn/scripts/service.sh
|
||||
```
|
||||
|
||||
### 添加执行权限
|
||||
```bash
|
||||
chmod +x /www/wwwroot/api.yunzer.cn/scripts/service.sh
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
```bash
|
||||
# 启动服务
|
||||
bash /www/wwwroot/api.yunzer.cn/scripts/service.sh start
|
||||
|
||||
# 停止服务
|
||||
bash /www/wwwroot/api.yunzer.cn/scripts/service.sh stop
|
||||
|
||||
# 重启服务
|
||||
bash /www/wwwroot/api.yunzer.cn/scripts/service.sh restart
|
||||
|
||||
# 查看状态
|
||||
bash /www/wwwroot/api.yunzer.cn/scripts/service.sh status
|
||||
|
||||
# 查看日志(最后 50 行)
|
||||
bash /www/wwwroot/api.yunzer.cn/scripts/service.sh logs
|
||||
|
||||
# 实时查看日志
|
||||
bash /www/wwwroot/api.yunzer.cn/scripts/service.sh logs -f
|
||||
|
||||
# 查看最后 100 行日志
|
||||
bash /www/wwwroot/api.yunzer.cn/scripts/service.sh logs 100
|
||||
```
|
||||
|
||||
### 创建快捷命令(可选)
|
||||
```bash
|
||||
# 添加到 ~/.bashrc
|
||||
echo 'alias go-service="bash /www/wwwroot/api.yunzer.cn/scripts/service.sh"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# 使用快捷命令
|
||||
go-service start
|
||||
go-service restart
|
||||
go-service status
|
||||
go-service logs -f
|
||||
```
|
||||
|
||||
## 方式三:后台直接启动
|
||||
|
||||
### 启动服务
|
||||
```bash
|
||||
cd /www/wwwroot/api.yunzer.cn
|
||||
nohup go run main.go &
|
||||
nohup go run main.go > go.log 2>&1 &
|
||||
```
|
||||
|
||||
查看是否运行成功
|
||||
### 查看是否运行成功
|
||||
```bash
|
||||
tail -f go.log
|
||||
```
|
||||
|
||||
### 查看进程
|
||||
```bash
|
||||
ps aux | grep "go run main.go" | grep -v grep
|
||||
```
|
||||
|
||||
下次要重启
|
||||
pkill go && cd /www/wwwroot/api.yunzer.cn && nohup go run main.go &
|
||||
### 重启服务
|
||||
```bash
|
||||
pkill -f "go run main.go" && cd /www/wwwroot/api.yunzer.cn && nohup go run main.go > go.log 2>&1 &
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
```bash
|
||||
pkill -f "go run main.go"
|
||||
```
|
||||
|
||||
## 日志查看
|
||||
|
||||
### 查看实时日志
|
||||
```bash
|
||||
# systemd 方式
|
||||
journalctl -u go-api -f
|
||||
|
||||
# 直接启动方式
|
||||
tail -f /www/wwwroot/api.yunzer.cn/go.log
|
||||
```
|
||||
|
||||
### 查看最近日志
|
||||
```bash
|
||||
# systemd 方式
|
||||
journalctl -u go-api -n 100
|
||||
|
||||
# 直接启动方式
|
||||
tail -n 100 /www/wwwroot/api.yunzer.cn/go.log
|
||||
```
|
||||
|
||||
### 查看错误日志
|
||||
```bash
|
||||
# systemd 方式
|
||||
journalctl -u go-api -p err
|
||||
|
||||
# 直接启动方式
|
||||
grep -i error /www/wwwroot/api.yunzer.cn/go.log
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 服务启动失败
|
||||
|
||||
**检查日志**:
|
||||
```bash
|
||||
# systemd
|
||||
journalctl -u go-api -n 50
|
||||
|
||||
# 直接启动
|
||||
tail -n 50 /www/wwwroot/api.yunzer.cn/go.log
|
||||
```
|
||||
|
||||
**常见原因**:
|
||||
- 端口被占用(8081)
|
||||
- 数据库连接失败
|
||||
- 配置文件错误
|
||||
|
||||
### 2. 端口被占用
|
||||
|
||||
**查看端口占用**:
|
||||
```bash
|
||||
netstat -tlnp | grep 8081
|
||||
# 或
|
||||
lsof -i :8081
|
||||
```
|
||||
|
||||
**停止占用进程**:
|
||||
```bash
|
||||
# 找到 PID
|
||||
lsof -i :8081
|
||||
|
||||
# 停止进程
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
### 3. 进程残留
|
||||
|
||||
**查找残留进程**:
|
||||
```bash
|
||||
ps aux | grep "go run main.go" | grep -v grep
|
||||
```
|
||||
|
||||
**清理残留进程**:
|
||||
```bash
|
||||
pkill -9 -f "go run main.go"
|
||||
```
|
||||
|
||||
### 4. 日志文件不存在
|
||||
|
||||
**原因**:启动命令没有重定向输出
|
||||
|
||||
**解决**:使用正确的启动命令
|
||||
```bash
|
||||
nohup go run main.go > go.log 2>&1 &
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
### 查看资源占用
|
||||
```bash
|
||||
# CPU 和内存
|
||||
top -p $(pgrep -f "go run main.go")
|
||||
|
||||
# 详细信息
|
||||
ps aux | grep "go run main.go" | grep -v grep
|
||||
```
|
||||
|
||||
### 查看连接数
|
||||
```bash
|
||||
netstat -an | grep 8081 | wc -l
|
||||
```
|
||||
|
||||
### 查看文件描述符
|
||||
```bash
|
||||
lsof -p $(pgrep -f "go run main.go") | wc -l
|
||||
```
|
||||
|
||||
## 生产环境建议
|
||||
|
||||
1. **使用 systemd 服务**:更稳定,支持自动重启
|
||||
2. **配置日志轮转**:防止日志文件过大
|
||||
3. **监控服务状态**:使用监控工具(如 Prometheus)
|
||||
4. **定期备份**:备份数据库和配置文件
|
||||
5. **使用编译后的二进制**:比 `go run` 更高效
|
||||
|
||||
### 编译并运行(推荐生产环境)
|
||||
```bash
|
||||
# 编译
|
||||
cd /www/wwwroot/api.yunzer.cn
|
||||
go build -o server main.go
|
||||
|
||||
# 运行
|
||||
nohup ./server > go.log 2>&1 &
|
||||
|
||||
# 或使用 systemd(修改 ExecStart)
|
||||
# ExecStart=/www/wwwroot/api.yunzer.cn/server
|
||||
```
|
||||
|
||||
## 更新日期
|
||||
|
||||
2026-04-09
|
||||
@@ -5,22 +5,21 @@ go 1.17
|
||||
require (
|
||||
github.com/beego/beego/v2 v2.1.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||
github.com/qiniu/go-sdk/v7 v7.18.2
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/smartystreets/goconvey v1.6.4
|
||||
github.com/google/uuid v1.6.0
|
||||
golang.org/x/net v0.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
@@ -29,8 +28,7 @@ require (
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
|
||||
@@ -152,6 +152,12 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
|
||||
github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
@@ -228,9 +234,10 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
@@ -286,7 +293,6 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
@@ -300,6 +306,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -307,6 +314,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
|
||||
github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
@@ -419,10 +427,16 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
|
||||
github.com/qiniu/go-sdk/v7 v7.18.2 h1:vk9eo5OO7aqgAOPF0Ytik/gt7CMKuNgzC/IPkhda6rk=
|
||||
github.com/qiniu/go-sdk/v7 v7.18.2/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=
|
||||
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
||||
github.com/rabbitmq/amqp091-go v1.2.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
@@ -438,9 +452,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
@@ -507,10 +519,11 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -592,6 +605,7 @@ golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
@@ -616,6 +630,7 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -685,12 +700,14 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -700,6 +717,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
_ "server/routers"
|
||||
"server/version"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化数据库
|
||||
models.Init(version.Version)
|
||||
|
||||
// CORS配置已移至router.go中统一管理
|
||||
// 确保请求体被正确读取(包括 POST、PUT、PATCH)
|
||||
beego.InsertFilter("*", beego.BeforeRouter, func(ctx *context.Context) {
|
||||
method := ctx.Input.Method()
|
||||
if method == "PUT" || method == "POST" || method == "PATCH" {
|
||||
uri := ctx.Request.URL.Path
|
||||
// 大文件 multipart 不能先 CopyBody 截断,否则上传解析失败
|
||||
if strings.Contains(uri, "/uploadfile") || strings.Contains(uri, "/uploadfiles") || strings.Contains(uri, "/uploadavatar") {
|
||||
return
|
||||
}
|
||||
ctx.Input.CopyBody(1024 * 1024) // 1MB 缓冲区
|
||||
}
|
||||
})
|
||||
// 启用请求体复制(允许多次读取请求体)
|
||||
beego.BConfig.CopyRequestBody = true
|
||||
|
||||
// 设置最大请求体大小(10MB,足够登录请求使用)
|
||||
beego.BConfig.MaxMemory = 10 << 20 // 10MB
|
||||
|
||||
// 静态资源:映射 /uploads 到本地 uploads 目录,供前端访问上传文件
|
||||
beego.SetStaticPath("/uploads", "uploads")
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// CmsArticleCategory CMS 文章分类 yz_cms_article_category
|
||||
type CmsArticleCategory struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"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);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 *CmsArticleCategory) TableName() string {
|
||||
return "yz_cms_article_category"
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func CmsFormatTime(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
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("delete_time__isnull", true).
|
||||
Filter("title__icontains", title).
|
||||
Limit(limit).
|
||||
All(&rows, "ID", "Title")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// BackendErpOrganization 组织架构表 yz_backend_erp_organization
|
||||
type BackendErpOrganization struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
OrgName string `orm:"column(org_name);size(128)" json:"org_name"`
|
||||
OrgCode string `orm:"column(org_code);size(64)" json:"org_code"`
|
||||
ParentID uint64 `orm:"column(parent_id);default(0)" json:"parent_id"`
|
||||
Sort uint `orm:"column(sort);default(0)" json:"sort"`
|
||||
LeaderID *uint64 `orm:"column(leader_id);null" json:"leader_id"`
|
||||
IsCompany int `orm:"column(is_company);default(0)" json:"is_company"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
Remark *string `orm:"column(remark);size(512);null" json:"remark"`
|
||||
}
|
||||
|
||||
// TableName 自定义表名
|
||||
func (m *BackendErpOrganization) TableName() string {
|
||||
return "yz_backend_erp_organization"
|
||||
}
|
||||
|
||||
// BackendErpEmployee 员工信息表 yz_backend_erp_employee
|
||||
type BackendErpEmployee struct {
|
||||
ID uint `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid *int `orm:"column(tid);null" json:"tid"`
|
||||
Account string `orm:"column(account);size(50)" json:"account"`
|
||||
Password string `orm:"column(password);size(64);default()" json:"-"`
|
||||
Name string `orm:"column(name);size(30)" json:"name"`
|
||||
Gender int8 `orm:"column(gender);default(0)" json:"gender"`
|
||||
Birthday *time.Time `orm:"column(birthday);type(date);null" json:"birthday"`
|
||||
AffiliateUnit *string `orm:"column(affiliate_unit);size(100);null" json:"affiliate_unit"`
|
||||
Department *string `orm:"column(department);size(50);null" json:"department"`
|
||||
Position *string `orm:"column(position);size(50);null" json:"position"`
|
||||
Education *string `orm:"column(education);size(20);null" json:"education"`
|
||||
Nation *string `orm:"column(nation);size(20);null" json:"nation"`
|
||||
Phone *string `orm:"column(phone);size(20);null" json:"phone"`
|
||||
Wechat *string `orm:"column(wechat);size(50);null" json:"wechat"`
|
||||
Email *string `orm:"column(email);size(100);null" json:"email"`
|
||||
HomeAddress *string `orm:"column(home_address);size(255);null" json:"home_address"`
|
||||
AccountStatus int8 `orm:"column(account_status);default(1)" json:"account_status"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
// TableName 自定义表名
|
||||
func (m *BackendErpEmployee) TableName() string {
|
||||
return "yz_backend_erp_employee"
|
||||
}
|
||||
|
||||
// BackendErpPosition 职位表 yz_backend_erp_position
|
||||
type BackendErpPosition struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID uint64 `orm:"column(tenant_id)" json:"tenant_id"`
|
||||
DepartmentID uint64 `orm:"column(department_id)" json:"department_id"`
|
||||
PositionCode string `orm:"column(position_code);size(50)" json:"position_code"`
|
||||
PositionName string `orm:"column(position_name);size(100)" json:"position_name"`
|
||||
PositionType int8 `orm:"column(position_type);default(0)" json:"position_type"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
Sort uint `orm:"column(sort);default(0)" json:"sort"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
}
|
||||
|
||||
// TableName 自定义表名
|
||||
func (m *BackendErpPosition) TableName() string {
|
||||
return "yz_backend_erp_position"
|
||||
}
|
||||
+13
-2
@@ -33,8 +33,11 @@ func Init(_ string) {
|
||||
|
||||
// 注册模型
|
||||
orm.RegisterModel(
|
||||
new(Tenant),
|
||||
new(TenantUser),
|
||||
new(SystemTenant),
|
||||
new(SystemTenantUser),
|
||||
new(BackendErpOrganization),
|
||||
new(BackendErpEmployee),
|
||||
new(BackendErpPosition),
|
||||
new(SystemMenu),
|
||||
new(AdminUser),
|
||||
new(AdminRole),
|
||||
@@ -48,10 +51,18 @@ func Init(_ string) {
|
||||
new(SystemTenantDomain),
|
||||
new(SystemModules),
|
||||
new(PlatformLoginVerify),
|
||||
new(StorageConfig),
|
||||
new(TenantSiteSetting),
|
||||
new(ComplaintCategory),
|
||||
new(PlatformComplaint),
|
||||
new(SystemSoftwareUpgrade),
|
||||
new(PlatformCursorEquipment),
|
||||
new(PlatformCursorActivationCode),
|
||||
new(PlatformAccountPoolKiro),
|
||||
new(PlatformAccountPoolWindsurf),
|
||||
new(PlatformAccountPoolCursor),
|
||||
new(CmsArticleCategory),
|
||||
new(CmsArticle),
|
||||
)
|
||||
|
||||
// 创建全局 Ormer
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// PlatformAccountPoolKiro 号池表: yz_platform_account_pool_krio
|
||||
type PlatformAccountPoolKiro struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
DataType string `orm:"column(data_type);size(32)" json:"data_type"` // account | tk | account_tk
|
||||
Account string `orm:"column(account);size(128);default()" json:"account"`
|
||||
Password string `orm:"column(password);size(255);default()" json:"password"`
|
||||
Token string `orm:"column(token);type(text);null" json:"token"`
|
||||
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
|
||||
IsExtracted int8 `orm:"column(is_extracted);default(0)" json:"is_extracted"`
|
||||
ExtractedTime *time.Time `orm:"column(extracted_time);type(datetime);null" json:"extracted_time"`
|
||||
ExtractedPlatform *string `orm:"column(extracted_platform);size(32);null" json:"extracted_platform"`
|
||||
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 *PlatformAccountPoolKiro) TableName() string {
|
||||
return "yz_platform_account_pool_krio"
|
||||
}
|
||||
|
||||
// PlatformAccountPoolWindsurf 号池表: yz_platform_account_pool_windsurf
|
||||
type PlatformAccountPoolWindsurf struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
DataType string `orm:"column(data_type);size(32)" json:"data_type"` // account | tk | account_tk
|
||||
Account string `orm:"column(account);size(128);default()" json:"account"`
|
||||
Password string `orm:"column(password);size(255);default()" json:"password"`
|
||||
Token string `orm:"column(token);type(text);null" json:"token"`
|
||||
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
|
||||
IsExtracted int8 `orm:"column(is_extracted);default(0)" json:"is_extracted"`
|
||||
ExtractedTime *time.Time `orm:"column(extracted_time);type(datetime);null" json:"extracted_time"`
|
||||
ExtractedPlatform *string `orm:"column(extracted_platform);size(32);null" json:"extracted_platform"`
|
||||
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 *PlatformAccountPoolWindsurf) TableName() string {
|
||||
return "yz_platform_account_pool_windsurf"
|
||||
}
|
||||
|
||||
// PlatformAccountPoolCursor 号池表: yz_platform_account_pool_cursor
|
||||
type PlatformAccountPoolCursor struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
DataType string `orm:"column(data_type);size(32)" json:"data_type"` // account | tk | account_tk
|
||||
Account string `orm:"column(account);size(128);default()" json:"account"`
|
||||
Password string `orm:"column(password);size(255);default()" json:"password"`
|
||||
Token string `orm:"column(token);type(text);null" json:"token"`
|
||||
Remark string `orm:"column(remark);size(255);default()" json:"remark"`
|
||||
IsExtracted int8 `orm:"column(is_extracted);default(0)" json:"is_extracted"`
|
||||
IsUsed *int8 `orm:"column(is_used);null" json:"is_used"` // 0=用完/不可用 1=可用 NULL=未探测
|
||||
ExtractedTime *time.Time `orm:"column(extracted_time);type(datetime);null" json:"extracted_time"`
|
||||
ExtractedPlatform *string `orm:"column(extracted_platform);size(32);null" json:"extracted_platform"`
|
||||
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 *PlatformAccountPoolCursor) TableName() string {
|
||||
return "yz_platform_account_pool_cursor"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// StorageConfig 存储配置(单行配置)
|
||||
type StorageConfig struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
StorageType string `orm:"column(storage_type);size(20);default(local)" json:"storage_type"` // local/qiniu
|
||||
// 七牛云配置
|
||||
QiniuAccessKey string `orm:"column(qiniu_access_key);size(255);null" json:"qiniu_access_key"`
|
||||
QiniuSecretKey string `orm:"column(qiniu_secret_key);size(255);null" json:"qiniu_secret_key"`
|
||||
QiniuBucket string `orm:"column(qiniu_bucket);size(128);null" json:"qiniu_bucket"`
|
||||
QiniuDomain string `orm:"column(qiniu_domain);size(255);null" json:"qiniu_domain"` // CDN域名
|
||||
QiniuRegion string `orm:"column(qiniu_region);size(50);null" json:"qiniu_region"` // 存储区域
|
||||
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"`
|
||||
}
|
||||
|
||||
func (m *StorageConfig) TableName() string {
|
||||
return "yz_system_storage_config"
|
||||
}
|
||||
|
||||
// GetStorageConfig 获取存储配置
|
||||
func GetStorageConfig() (*StorageConfig, error) {
|
||||
var cfg StorageConfig
|
||||
err := Orm.QueryTable(new(StorageConfig)).OrderBy("-id").One(&cfg)
|
||||
if err != nil {
|
||||
// 默认配置:本地存储
|
||||
return &StorageConfig{StorageType: "local"}, nil
|
||||
}
|
||||
if cfg.StorageType == "" {
|
||||
cfg.StorageType = "local"
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type SystemTenant struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantCode string `orm:"column(tenant_code);size(32)" json:"tenant_code"`
|
||||
TenantName string `orm:"column(tenant_name);size(128)" json:"tenant_name"`
|
||||
ContactPerson *string `orm:"column(contact_person);size(64);null" json:"contact_person"`
|
||||
ContactPhone *string `orm:"column(contact_phone);size(20);null" json:"contact_phone"`
|
||||
ContactEmail *string `orm:"column(contact_email);size(128);null" json:"contact_email"`
|
||||
Address *string `orm:"column(address);size(255);null" json:"address"`
|
||||
Worktime *string `orm:"column(worktime);size(255);null" json:"worktime"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
Remark *string `orm:"column(remark);size(512);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *SystemTenant) TableName() string {
|
||||
return "yz_system_tenant"
|
||||
}
|
||||
@@ -16,5 +16,5 @@ type SystemTenantDomain struct {
|
||||
}
|
||||
|
||||
func (m *SystemTenantDomain) TableName() string {
|
||||
return "yz_tenant_domain"
|
||||
return "yz_system_tenant_domain"
|
||||
}
|
||||
|
||||
@@ -26,6 +26,5 @@ type TenantSiteSetting struct {
|
||||
}
|
||||
|
||||
func (m *TenantSiteSetting) TableName() string {
|
||||
return "yz_tenant_site_setting"
|
||||
return "yz_system_tenant_site_setting"
|
||||
}
|
||||
|
||||
@@ -2,25 +2,25 @@ package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantUser 租户用户绑定关系表 yz_system_tenant_user
|
||||
type TenantUser struct {
|
||||
type SystemTenantUser struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"` // 租户ID
|
||||
Uid uint64 `orm:"column(uid)" json:"uid"` // 用户ID
|
||||
Account *string `orm:"column(account);size(64);null" json:"account"` // 用户账号(冗余)
|
||||
Name *string `orm:"column(name);size(64);null" json:"name"` // 用户名称(冗余)
|
||||
Phone *string `orm:"column(phone);size(20);null" json:"phone"` // 手机号(冗余)
|
||||
Email *string `orm:"column(email);size(128);null" json:"email"` // 邮箱(冗余)
|
||||
Password *string `orm:"column(password);size(255);null" json:"password"` // 密码(冗余/可选)
|
||||
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"` // 是否默认租户
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 状态:1启用,0禁用
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
Uid uint64 `orm:"column(uid)" json:"uid"`
|
||||
Account *string `orm:"column(account);size(64);null" json:"account"`
|
||||
Name *string `orm:"column(name);size(64);null" json:"name"`
|
||||
Phone *string `orm:"column(phone);size(20);null" json:"phone"`
|
||||
Email *string `orm:"column(email);size(128);null" json:"email"`
|
||||
Sex uint8 `orm:"column(sex);default(0)" json:"sex"`
|
||||
Birth *string `orm:"column(birth);size(20);null" json:"birth"`
|
||||
Password *string `orm:"column(password);size(255);null" json:"password"`
|
||||
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
Remark *string `orm:"column(remark);size(255);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
// TableName 自定义表名
|
||||
func (m *TenantUser) TableName() string {
|
||||
func (m *SystemTenantUser) TableName() string {
|
||||
return "yz_system_tenant_user"
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Tenant 租户表 yz_system_tenant
|
||||
type Tenant struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"` // 租户唯一标识(主键)
|
||||
TenantCode string `orm:"column(tenant_code);size(32);unique" json:"tenantCode"` // 租户编码
|
||||
TenantName string `orm:"column(tenant_name);size(128)" json:"tenantName"` // 租户名称
|
||||
ContactPerson string `orm:"column(contact_person);size(64);null" json:"contactPerson"` // 联系人
|
||||
ContactPhone string `orm:"column(contact_phone);size(20);null" json:"contactPhone"` // 联系电话
|
||||
ContactEmail string `orm:"column(contact_email);size(128);null" json:"contactEmail"` // 联系邮箱
|
||||
Address string `orm:"column(address);size(255);null" json:"address"` // 租户地址
|
||||
Worktime string `orm:"column(worktime);size(255);null" json:"worktime"` // 工作时间
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 租户状态:1-正常,2-停用,0-删除
|
||||
Remark string `orm:"column(remark);size(512);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"` // 删除时间
|
||||
}
|
||||
|
||||
// TableName 自定义表名
|
||||
func (t *Tenant) TableName() string {
|
||||
return "yz_system_tenant"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
2026/04/09 17:41:49.470 [1;34m[I][0m [server.go:281] http server Running on http://:8081
|
||||
2026/04/09 17:43:09.442 [1;34m[I][0m [server.go:281] http server Running on http://:8081
|
||||
2026/04/09 17:43:09.442 [1;35m[C][0m [server.go:298] ListenAndServe: listen tcp :8081: bind: address already in use
|
||||
2026/04/09 17:43:15.715 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 265.592518ms| match|[97;44m GET [0m /platform/usercate r:/platform/usercate
|
||||
2026/04/09 17:43:15.925 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 468.017304ms| match|[97;44m GET [0m /platform/currentUser r:/platform/currentUser
|
||||
2026/04/09 17:43:16.057 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 274.492712ms| match|[97;44m GET [0m /platform/catefiles/0 r:/platform/catefiles/:id
|
||||
2026/04/09 17:43:22.620 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 277.387093ms| match|[97;44m GET [0m /platform/usercate r:/platform/usercate
|
||||
2026/04/09 17:43:22.622 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 271.734643ms| match|[97;44m GET [0m /platform/currentUser r:/platform/currentUser
|
||||
2026/04/09 17:43:23.037 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 353.76378ms| match|[97;44m GET [0m /platform/catefiles/0 r:/platform/catefiles/:id
|
||||
2026/04/09 17:43:24.492 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 351.839484ms| match|[97;44m GET [0m /platform/catefiles/5 r:/platform/catefiles/:id
|
||||
2026/04/09 17:43:25.518 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 325.277959ms| match|[97;44m GET [0m /platform/catefiles/0 r:/platform/catefiles/:id
|
||||
2026/04/09 17:43:29.495 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 10.772µs| nomatch|[90;47m OPTIONS [0m /platform/logout
|
||||
2026/04/09 17:43:29.537 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 42.423µs| match|[97;46m POST [0m /platform/logout r:/platform/logout
|
||||
2026/04/09 17:43:29.744 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 88.149485ms| match|[97;44m GET [0m /platform/loginVerifyInfos r:/platform/loginVerifyInfos
|
||||
2026/04/09 17:43:30.734 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 87.869914ms| match|[97;44m GET [0m /platform/login/getOpenVerify r:/platform/login/getOpenVerify
|
||||
2026/04/09 17:43:30.868 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 87.927568ms| match|[97;44m GET [0m /platform/login/getGeetest4Infos r:/platform/login/getGeetest4Infos
|
||||
2026/04/09 17:43:36.373 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 61.401µs| match|[97;46m POST [0m /platform/login r:/platform/login
|
||||
2026/04/09 17:43:50.709 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 89.443767ms| match|[97;44m GET [0m /platform/loginVerifyInfos r:/platform/loginVerifyInfos
|
||||
2026/04/09 17:43:52.056 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 70.997714ms| match|[97;44m GET [0m /platform/login/getOpenVerify r:/platform/login/getOpenVerify
|
||||
2026/04/09 17:43:52.175 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 74.287857ms| match|[97;44m GET [0m /platform/login/getGeetest4Infos r:/platform/login/getGeetest4Infos
|
||||
2026/04/09 17:43:57.811 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 11.75µs| nomatch|[90;47m OPTIONS [0m /platform/login
|
||||
2026/04/09 17:43:57.854 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 42.819µs| match|[97;46m POST [0m /platform/login r:/platform/login
|
||||
2026/04/09 17:43:59.739 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 70.336904ms| match|[97;44m GET [0m /platform/login/getOpenVerify r:/platform/login/getOpenVerify
|
||||
2026/04/09 17:43:59.862 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 70.920607ms| match|[97;44m GET [0m /platform/login/getGeetest4Infos r:/platform/login/getGeetest4Infos
|
||||
2026/04/09 17:44:05.289 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 11.177µs| nomatch|[90;47m OPTIONS [0m /platform/login
|
||||
2026/04/09 17:44:05.332 [1;44m[D][0m [router.go:1305] | 127.0.0.1|[97;42m 200 [0m| 42.979µs| match|[97;46m POST [0m /platform/login r:/platform/login
|
||||
@@ -0,0 +1,560 @@
|
||||
package tokenprobe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
cursorBackendURL = "https://api2.cursor.sh"
|
||||
cursorAgentPath = "/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
|
||||
cursorClientVersion = "2.6.22"
|
||||
cursorHiMaxRead = 512 * 1024
|
||||
// probeHiText 发往官方 Agent 的探测内容(与前端展示 probeMessage 一致)
|
||||
probeHiText = "hi"
|
||||
)
|
||||
|
||||
var cursorProbeHTTPClient = newCursorHTTP2Client()
|
||||
|
||||
func newCursorHTTP2Client() *http.Client {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
|
||||
}
|
||||
// 与 Cursor 官方一致走 HTTP/2
|
||||
if err := http2.ConfigureTransport(tr); err != nil {
|
||||
return &http.Client{Timeout: 40 * time.Second}
|
||||
}
|
||||
return &http.Client{Transport: tr, Timeout: 40 * time.Second}
|
||||
}
|
||||
|
||||
func cursorClientOS() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return "win32"
|
||||
case "darwin":
|
||||
return "darwin"
|
||||
default:
|
||||
return "linux"
|
||||
}
|
||||
}
|
||||
|
||||
func cursorClientArch() string {
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
return "x64"
|
||||
case "arm64":
|
||||
return "arm64"
|
||||
default:
|
||||
return runtime.GOARCH
|
||||
}
|
||||
}
|
||||
|
||||
func cursorEnvVersion() string {
|
||||
if v := strings.TrimSpace(os.Getenv("CURSOR_CLIENT_VERSION")); v != "" {
|
||||
return v
|
||||
}
|
||||
return cursorClientVersion
|
||||
}
|
||||
|
||||
// --- protobuf wire (与 cursor_api_demo 对齐) ---
|
||||
|
||||
func pbVarint(v uint64) []byte {
|
||||
var out []byte
|
||||
for v >= 0x80 {
|
||||
out = append(out, byte(v&0x7f|0x80))
|
||||
v >>= 7
|
||||
}
|
||||
out = append(out, byte(v&0x7f))
|
||||
return out
|
||||
}
|
||||
|
||||
func pbField(fieldNum int, wireType int, value interface{}) []byte {
|
||||
tag := uint64(fieldNum<<3 | wireType)
|
||||
out := pbVarint(tag)
|
||||
switch wireType {
|
||||
case 0:
|
||||
var n uint64
|
||||
switch x := value.(type) {
|
||||
case int:
|
||||
n = uint64(x)
|
||||
case int32:
|
||||
n = uint64(x)
|
||||
case uint32:
|
||||
n = uint64(x)
|
||||
case uint64:
|
||||
n = x
|
||||
default:
|
||||
n = uint64(0)
|
||||
}
|
||||
out = append(out, pbVarint(n)...)
|
||||
case 2:
|
||||
var b []byte
|
||||
switch x := value.(type) {
|
||||
case string:
|
||||
b = []byte(x)
|
||||
case []byte:
|
||||
b = x
|
||||
default:
|
||||
b = []byte(fmt.Sprint(x))
|
||||
}
|
||||
out = append(out, pbVarint(uint64(len(b)))...)
|
||||
out = append(out, b...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func encodeCursorMessage(content string, role int, messageID string, chatModeEnum *int) []byte {
|
||||
msg := pbField(1, 2, content)
|
||||
msg = append(msg, pbField(2, 0, role)...)
|
||||
msg = append(msg, pbField(13, 2, messageID)...)
|
||||
if chatModeEnum != nil {
|
||||
msg = append(msg, pbField(47, 0, *chatModeEnum)...)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func encodeCursorModel(modelName string) []byte {
|
||||
msg := pbField(1, 2, modelName)
|
||||
msg = append(msg, pbField(4, 2, []byte{})...)
|
||||
return msg
|
||||
}
|
||||
|
||||
func encodeCursorSetting() []byte {
|
||||
inner := pbField(1, 2, []byte{})
|
||||
inner = append(inner, pbField(2, 2, []byte{})...)
|
||||
msg := pbField(1, 2, `cursor\aisettings`)
|
||||
msg = append(msg, pbField(3, 2, []byte{})...)
|
||||
msg = append(msg, pbField(6, 2, inner)...)
|
||||
msg = append(msg, pbField(8, 0, 1)...)
|
||||
msg = append(msg, pbField(9, 0, 1)...)
|
||||
return msg
|
||||
}
|
||||
|
||||
func encodeCursorMetadata() []byte {
|
||||
msg := pbField(1, 2, cursorClientOS())
|
||||
msg = append(msg, pbField(2, 2, cursorClientArch())...)
|
||||
msg = append(msg, pbField(3, 2, "unknown")...)
|
||||
msg = append(msg, pbField(4, 2, "go-platform/tokenprobe")...)
|
||||
msg = append(msg, pbField(5, 2, time.Now().Format(time.RFC3339))...)
|
||||
return msg
|
||||
}
|
||||
|
||||
func encodeCursorMessageID(messageID string, role int) []byte {
|
||||
msg := pbField(1, 2, messageID)
|
||||
msg = append(msg, pbField(3, 0, role)...)
|
||||
return msg
|
||||
}
|
||||
|
||||
// defaultAgentTools 与 cursor_agent_client.DEFAULT_TOOLS 一致
|
||||
var defaultAgentTools = []int{5, 6, 3, 15, 7, 8, 42}
|
||||
|
||||
func encodeCursorAgentRequest(userContent, modelName string) []byte {
|
||||
msgID := uuid.NewString()
|
||||
cm := 2 // Agent
|
||||
userMsg := encodeCursorMessage(userContent, 1, msgID, &cm)
|
||||
|
||||
var msg []byte
|
||||
msg = append(msg, pbField(1, 2, userMsg)...)
|
||||
msg = append(msg, pbField(2, 0, 1)...)
|
||||
msg = append(msg, pbField(3, 2, []byte{})...)
|
||||
msg = append(msg, pbField(4, 0, 1)...)
|
||||
msg = append(msg, pbField(5, 2, encodeCursorModel(modelName))...)
|
||||
msg = append(msg, pbField(8, 2, "")...)
|
||||
msg = append(msg, pbField(13, 0, 1)...)
|
||||
msg = append(msg, pbField(15, 2, encodeCursorSetting())...)
|
||||
msg = append(msg, pbField(19, 0, 1)...)
|
||||
msg = append(msg, pbField(23, 2, uuid.NewString())...)
|
||||
msg = append(msg, pbField(26, 2, encodeCursorMetadata())...)
|
||||
msg = append(msg, pbField(27, 0, 1)...)
|
||||
for _, t := range defaultAgentTools {
|
||||
msg = append(msg, pbField(29, 0, t)...)
|
||||
}
|
||||
msg = append(msg, pbField(30, 2, encodeCursorMessageID(msgID, 1))...)
|
||||
msg = append(msg, pbField(35, 0, 0)...)
|
||||
msg = append(msg, pbField(38, 0, 0)...)
|
||||
msg = append(msg, pbField(46, 0, 2)...)
|
||||
msg = append(msg, pbField(47, 2, "")...)
|
||||
msg = append(msg, pbField(48, 0, 0)...)
|
||||
msg = append(msg, pbField(49, 0, 0)...)
|
||||
msg = append(msg, pbField(51, 0, 0)...)
|
||||
msg = append(msg, pbField(53, 0, 1)...)
|
||||
msg = append(msg, pbField(54, 2, "agent")...)
|
||||
return msg
|
||||
}
|
||||
|
||||
func encodeStreamUnifiedChatWithToolsRequest(inner []byte) []byte {
|
||||
return pbField(1, 2, inner)
|
||||
}
|
||||
|
||||
func generateCursorAgentFramedBody(userText, model string) []byte {
|
||||
inner := encodeCursorAgentRequest(userText, model)
|
||||
buf := encodeStreamUnifiedChatWithToolsRequest(inner)
|
||||
magic := byte(0x00)
|
||||
hexLen := fmt.Sprintf("%08x", len(buf))
|
||||
lenBytes, err := hex.DecodeString(hexLen)
|
||||
if err != nil || len(lenBytes) != 4 {
|
||||
lenB := []byte{byte(len(buf) >> 24), byte(len(buf) >> 16), byte(len(buf) >> 8), byte(len(buf))}
|
||||
return append([]byte{magic}, append(lenB, buf...)...)
|
||||
}
|
||||
return append([]byte{magic}, append(lenBytes, buf...)...)
|
||||
}
|
||||
|
||||
func hashed64Hex(input, salt string) string {
|
||||
h := sha256.Sum256([]byte(input + salt))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func generateCursorChecksum(authToken string) string {
|
||||
machineID := hashed64Hex(authToken, "machineId")
|
||||
ts := int(time.Now().UnixMilli() / 1_000_000)
|
||||
barr := []byte{
|
||||
byte(ts >> 40), byte(ts >> 32), byte(ts >> 24), byte(ts >> 16), byte(ts >> 8), byte(ts),
|
||||
}
|
||||
t := byte(165)
|
||||
for i := range barr {
|
||||
barr[i] = ((barr[i] ^ t) + byte(i%256)) & 255
|
||||
t = barr[i]
|
||||
}
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
var enc strings.Builder
|
||||
for i := 0; i < len(barr); i += 3 {
|
||||
a := barr[i]
|
||||
var b, c byte
|
||||
if i+1 < len(barr) {
|
||||
b = barr[i+1]
|
||||
}
|
||||
if i+2 < len(barr) {
|
||||
c = barr[i+2]
|
||||
}
|
||||
enc.WriteByte(alphabet[a>>2])
|
||||
enc.WriteByte(alphabet[((a&3)<<4)|(b>>4)])
|
||||
if i+1 < len(barr) {
|
||||
enc.WriteByte(alphabet[((b&15)<<2)|(c>>6)])
|
||||
}
|
||||
if i+2 < len(barr) {
|
||||
enc.WriteByte(alphabet[c&63])
|
||||
}
|
||||
}
|
||||
return enc.String() + machineID
|
||||
}
|
||||
|
||||
func asciiLowerInPlace(b []byte) {
|
||||
for i := range b {
|
||||
c := b[i]
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
b[i] = c + ('a' - 'A')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 社区脚本中的「额度用尽」ASCII 前缀(与明文一致,便于在二进制流中 bytes.Contains,无需整句)
|
||||
// 对应明文前缀:Get Cursor Pro for more Agent usage
|
||||
var cursorQuotaExhaustedSigCommunity = []byte{
|
||||
0x47, 0x65, 0x74, 0x20, 0x43, 0x75, 0x72, 0x73,
|
||||
0x6f, 0x72, 0x20, 0x50, 0x72, 0x6f, 0x20, 0x66,
|
||||
0x6f, 0x72, 0x20, 0x6d, 0x6f, 0x72, 0x65, 0x20,
|
||||
0x41, 0x67, 0x65, 0x6e, 0x74, 0x20, 0x75, 0x73,
|
||||
0x61, 0x67, 0x65,
|
||||
}
|
||||
|
||||
// cursorQuotaTipSig 与常见示例一致:raw 全字节里 bytes.Contains(raw, tipSig) → 额度用尽
|
||||
var cursorQuotaTipSig = []byte("Get Cursor Pro for more Agent usage, unlimited Tab, and more.")
|
||||
|
||||
const cursorLimitTipPrefix = "Get Cursor Pro for more Agent usage, unlimited Tab"
|
||||
|
||||
// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示(ASCII 区不区分大小写 + UTF-8 短语)
|
||||
func classifyCursorRawStream(raw []byte) (blocked bool, reason string) {
|
||||
if len(raw) == 0 {
|
||||
return false, ""
|
||||
}
|
||||
for _, sig := range cursorQuotaExhaustedSigsFromEnv() {
|
||||
if bytes.Contains(raw, sig) {
|
||||
return true, fmt.Sprintf("流中匹配:CURSOR_QUOTA_EXHAUSTED_SIG_HEX 配置的二进制特征(%d 字节)", len(sig))
|
||||
}
|
||||
}
|
||||
if bytes.Contains(raw, cursorQuotaTipSig) {
|
||||
return true, "流中匹配:" + string(cursorQuotaTipSig)
|
||||
}
|
||||
// 社区脚本:仅到「…Agent usage」的 ASCII 前缀(流里可能只有前半段)
|
||||
if bytes.Contains(raw, cursorQuotaExhaustedSigCommunity) {
|
||||
return true, "流中匹配:Get Cursor Pro for more Agent usage…(社区 QuotaExhaustedSignature 前缀)"
|
||||
}
|
||||
if bytes.Contains(raw, []byte(cursorLimitTipPrefix)) {
|
||||
return true, "流中匹配:" + cursorLimitTipPrefix + "…"
|
||||
}
|
||||
low := append([]byte(nil), raw...)
|
||||
asciiLowerInPlace(low)
|
||||
if bytes.Contains(low, []byte("you've hit your usage limit")) ||
|
||||
bytes.Contains(low, []byte("youve hit your usage limit")) ||
|
||||
bytes.Contains(low, []byte("hit your usage limit")) {
|
||||
return true, "流中匹配:hit your usage limit / you've hit your usage limit"
|
||||
}
|
||||
if bytes.Contains(low, []byte("get cursor pro for more agent usage")) {
|
||||
return true, "流中匹配:get cursor pro for more agent usage"
|
||||
}
|
||||
if bytes.Contains(low, []byte("upgrade to pro")) {
|
||||
return true, "流中匹配:upgrade to pro"
|
||||
}
|
||||
if bytes.Contains(low, []byte("get cursor pro")) && bytes.Contains(low, []byte("agent")) {
|
||||
return true, "流中匹配:get cursor pro + agent"
|
||||
}
|
||||
if bytes.Contains(low, []byte("usage limit")) {
|
||||
return true, "流中匹配:usage limit"
|
||||
}
|
||||
if bytes.Contains(low, []byte("unlimited tab")) && bytes.Contains(low, []byte("cursor pro")) {
|
||||
return true, "流中匹配:unlimited tab + cursor pro"
|
||||
}
|
||||
|
||||
flat := strings.ToLower(strings.ToValidUTF8(string(raw), "\uFFFD"))
|
||||
flat = strings.ReplaceAll(flat, "\u2019", "'") // 右单引号
|
||||
flat = strings.ReplaceAll(flat, "`", "'")
|
||||
if strings.Contains(flat, "you've hit your usage limit") {
|
||||
return true, "流中匹配:you've hit your usage limit(UTF-8)"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func truncateUTF8Preview(raw []byte, maxBytes int) string {
|
||||
s := strings.ToValidUTF8(string(raw), "\uFFFD")
|
||||
if maxBytes <= 0 || len(s) <= maxBytes {
|
||||
return s
|
||||
}
|
||||
// 按字节截断并保证合法 UTF-8
|
||||
s = s[:maxBytes]
|
||||
for len(s) > 0 && !utf8.ValidString(s) {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s + "…(已截断)"
|
||||
}
|
||||
|
||||
func prefixHexBody(b []byte, max int) string {
|
||||
if len(b) > max {
|
||||
b = b[:max]
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func looksLikeGzip(raw []byte) bool {
|
||||
return len(raw) >= 3 && raw[0] == 0x1f && raw[1] == 0x8b && raw[2] == 0x08
|
||||
}
|
||||
|
||||
func gunzipBytes(raw []byte) ([]byte, error) {
|
||||
zr, err := gzip.NewReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zr.Close()
|
||||
return io.ReadAll(io.LimitReader(zr, cursorHiMaxRead))
|
||||
}
|
||||
|
||||
func decodeConnectFramedBody(raw []byte) ([]byte, string, bool) {
|
||||
if len(raw) < 5 {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
offset := 0
|
||||
frameCount := 0
|
||||
compressedFrames := 0
|
||||
|
||||
for offset+5 <= len(raw) {
|
||||
flags := raw[offset]
|
||||
n := int(raw[offset+1])<<24 | int(raw[offset+2])<<16 | int(raw[offset+3])<<8 | int(raw[offset+4])
|
||||
offset += 5
|
||||
if n < 0 || offset+n > len(raw) {
|
||||
return nil, "", false
|
||||
}
|
||||
payload := raw[offset : offset+n]
|
||||
offset += n
|
||||
frameCount++
|
||||
|
||||
isCompressed := flags&0x01 == 0x01
|
||||
if isCompressed || looksLikeGzip(payload) {
|
||||
decoded, err := gunzipBytes(payload)
|
||||
if err != nil {
|
||||
out.Write(payload)
|
||||
} else {
|
||||
out.Write(decoded)
|
||||
compressedFrames++
|
||||
}
|
||||
} else {
|
||||
out.Write(payload)
|
||||
}
|
||||
}
|
||||
|
||||
if frameCount == 0 || offset != len(raw) {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
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, 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 {
|
||||
if strings.Contains(enc, "gzip") {
|
||||
return raw, "响应头声明 gzip,但解压失败,已回退为原始字节预览"
|
||||
}
|
||||
return raw, "检测到 gzip 魔数,但解压失败,已回退为原始字节预览"
|
||||
}
|
||||
if strings.Contains(enc, "gzip") {
|
||||
return decoded, "响应体已按 gzip 解压后分析"
|
||||
}
|
||||
return decoded, "响应体虽未显式声明 Content-Encoding,但按 gzip 魔数解压后分析"
|
||||
}
|
||||
if enc != "" {
|
||||
return raw, "响应头 Content-Encoding=" + enc + ",当前未额外解码,按原始字节分析"
|
||||
}
|
||||
return raw, "响应体未压缩或未声明压缩,且未识别为 Connect 分帧,按原始字节分析"
|
||||
}
|
||||
|
||||
// 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)。` +
|
||||
`【响应】正文为分包二进制流,rawPreview 是按 UTF-8 有损解码的片段,绝大多数情况下会像乱码,属正常现象,不能当普通 UTF-8 接口正文解析。` +
|
||||
`【HTTP 200】仅表示 TLS/代理/网络到 api2.cursor.sh 通畅,不代表 protobuf 业务层、鉴权、设备指纹、风控配额或「能持续对话」已全部通过。` +
|
||||
`【ok 字段】当前仅在解码片段上做英文关键词启发式匹配;未命中不代表账户可用,命中也不覆盖「须在 IDE 内完整走流式协议」的场景。` +
|
||||
`【若要等价客户端】需完整实现 Connect 帧解析、会话与校验头、可能的 gzip/分包及双向流,本探测只做粗连通与可观测性辅助。` +
|
||||
`【二进制特征】若提示语被包在 protobuf 字段内、ASCII 子串匹配不到,可在运行环境设置 CURSOR_QUOTA_EXHAUSTED_SIG_HEX=hex1,hex2(逗号分隔十六进制,可选 0x 前缀),在原始响应字节上做 bytes.Contains,无需解析整条 proto;特征需自行对比「额度正常」与「用尽」两次抓包提取。`
|
||||
|
||||
// cursorQuotaExhaustedSigsFromEnv 从环境变量解析额度用尽时的二进制特征(不转 UTF-8)
|
||||
func cursorQuotaExhaustedSigsFromEnv() [][]byte {
|
||||
s := strings.TrimSpace(os.Getenv("CURSOR_QUOTA_EXHAUSTED_SIG_HEX"))
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
var out [][]byte
|
||||
for _, part := range strings.Split(s, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
part = strings.TrimPrefix(strings.TrimPrefix(part, "0x"), "0X")
|
||||
b, err := hex.DecodeString(part)
|
||||
if err != nil || len(b) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, b)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cursorProbeResult(ok bool, detail string, httpStatus int, reqBody, raw, preview []byte) Result {
|
||||
if preview == nil {
|
||||
preview = raw
|
||||
}
|
||||
return Result{
|
||||
OK: ok,
|
||||
Detail: detail,
|
||||
HTTPStatus: httpStatus,
|
||||
ProbeMessage: probeHiText,
|
||||
Endpoint: cursorBackendURL + cursorAgentPath,
|
||||
BytesRead: len(raw),
|
||||
RawPreview: truncateUTF8Preview(preview, 24000),
|
||||
RequestBodyPrefixHex: prefixHexBody(reqBody, 128),
|
||||
StreamProtocol: cursorStreamProtocol,
|
||||
StreamNote: cursorStreamNote,
|
||||
}
|
||||
}
|
||||
|
||||
func probeCursorHiAgent(authToken string) Result {
|
||||
if strings.Contains(authToken, "::") {
|
||||
if i := strings.LastIndex(authToken, "::"); i >= 0 {
|
||||
authToken = strings.TrimSpace(authToken[i+2:])
|
||||
}
|
||||
}
|
||||
if authToken == "" {
|
||||
return Result{OK: false, Detail: "Token 为空"}
|
||||
}
|
||||
|
||||
sessionID := uuid.NewSHA1(uuid.NameSpaceDNS, []byte(authToken)).String()
|
||||
clientKey := hashed64Hex(authToken, "")
|
||||
checksum := generateCursorChecksum(authToken)
|
||||
conversationID := uuid.NewString()
|
||||
reqID := uuid.NewString()
|
||||
|
||||
body := generateCursorAgentFramedBody(probeHiText, "default")
|
||||
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)
|
||||
return r
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
req.Header.Set("Connect-Accept-Encoding", "gzip")
|
||||
req.Header.Set("Connect-Protocol-Version", "1")
|
||||
req.Header.Set("Content-Type", "application/connect+proto")
|
||||
req.Header.Set("User-Agent", "connect-es/1.6.1")
|
||||
req.Header.Set("X-Amzn-Trace-Id", "Root="+reqID)
|
||||
req.Header.Set("X-Client-Key", clientKey)
|
||||
req.Header.Set("X-Cursor-Checksum", checksum)
|
||||
req.Header.Set("X-Cursor-Client-Version", cursorEnvVersion())
|
||||
req.Header.Set("X-Cursor-Client-Type", "ide")
|
||||
req.Header.Set("X-Cursor-Client-Os", cursorClientOS())
|
||||
req.Header.Set("X-Cursor-Client-Arch", cursorClientArch())
|
||||
req.Header.Set("X-Cursor-Client-Os-Version", "unknown")
|
||||
req.Header.Set("X-Cursor-Client-Device-Type", "desktop")
|
||||
req.Header.Set("X-Cursor-Config-Version", uuid.NewString())
|
||||
req.Header.Set("X-Cursor-Timezone", "UTC")
|
||||
req.Header.Set("X-Ghost-Mode", "false")
|
||||
req.Header.Set("X-New-Onboarding-Completed", "true")
|
||||
req.Header.Set("X-Request-Id", reqID)
|
||||
req.Header.Set("X-Session-Id", sessionID)
|
||||
req.Header.Set("X-Conversation-Id", conversationID)
|
||||
req.Host = "api2.cursor.sh"
|
||||
|
||||
resp, err := cursorProbeHTTPClient.Do(req)
|
||||
if err != 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, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
blocked, reason := classifyCursorRawStream(decoded)
|
||||
if blocked {
|
||||
return cursorProbeResult(false, reason+";"+decodeNote, 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, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
blocked, reason := classifyCursorRawStream(decoded)
|
||||
if blocked {
|
||||
return cursorProbeResult(false, reason+";"+decodeNote, resp.StatusCode, body, raw, decoded)
|
||||
}
|
||||
detail := "HTTP 200;未命中内置英文关键词;" + decodeNote + ";二进制流含义与 ok 边界见 streamNote"
|
||||
return cursorProbeResult(true, detail, resp.StatusCode, body, raw, decoded)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package tokenprobe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Timeout: 12 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
OK bool `json:"ok"`
|
||||
Detail string `json:"detail"`
|
||||
HTTPStatus int `json:"httpStatus"`
|
||||
ProbeMessage string `json:"probeMessage,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
BytesRead int `json:"bytesRead,omitempty"`
|
||||
RawPreview string `json:"rawPreview,omitempty"`
|
||||
RequestBodyPrefixHex string `json:"requestBodyPrefixHex,omitempty"`
|
||||
StreamProtocol string `json:"streamProtocol,omitempty"`
|
||||
StreamNote string `json:"streamNote,omitempty"`
|
||||
}
|
||||
|
||||
func ProbeOfficial(module, rawToken string) Result {
|
||||
tok := normalizeBearerToken(strings.TrimSpace(rawToken))
|
||||
if tok == "" {
|
||||
return Result{OK: false, Detail: "Token 为空"}
|
||||
}
|
||||
switch module {
|
||||
case "cursor":
|
||||
return probeCursor(tok)
|
||||
case "windsurf":
|
||||
return probeWindsurf(tok)
|
||||
case "krio":
|
||||
return probeKiro(tok)
|
||||
default:
|
||||
return Result{OK: false, Detail: "未知模块"}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBearerToken(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if i := strings.LastIndex(s, "::"); i >= 0 {
|
||||
return strings.TrimSpace(s[i+2:])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func probeCursor(token string) Result {
|
||||
return probeCursorHiAgent(token)
|
||||
}
|
||||
|
||||
func probeWindsurf(apiKey string) Result {
|
||||
payload := map[string]interface{}{
|
||||
"metadata": map[string]string{
|
||||
"apiKey": apiKey,
|
||||
"ideName": "windsurf",
|
||||
"ideVersion": "0.0.0",
|
||||
"extensionName": "windsurf",
|
||||
"extensionVersion": "0.0.0",
|
||||
"locale": "zh",
|
||||
},
|
||||
}
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: err.Error()}
|
||||
}
|
||||
req, err := http.NewRequest(
|
||||
http.MethodPost,
|
||||
"https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetUserStatus",
|
||||
bytes.NewReader(raw),
|
||||
)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: err.Error()}
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Connect-Protocol-Version", "1")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: "请求失败: " + err.Error()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var wrap map[string]interface{}
|
||||
if json.Unmarshal(body, &wrap) == nil {
|
||||
if _, ok := wrap["userStatus"]; ok {
|
||||
return Result{OK: true, Detail: "Codeium 云端接口响应正常", HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
}
|
||||
if bytes.Contains(body, []byte(`"planStatus"`)) || bytes.Contains(body, []byte(`"userStatus"`)) {
|
||||
return Result{OK: true, Detail: "Codeium 云端接口响应正常", HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
return Result{OK: true, Detail: fmt.Sprintf("HTTP %d,已收到响应", resp.StatusCode), HTTPStatus: resp.StatusCode}
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return Result{OK: false, Detail: fmt.Sprintf("API Key 无效或已失效(HTTP %d)", resp.StatusCode), HTTPStatus: resp.StatusCode}
|
||||
default:
|
||||
snip := strings.TrimSpace(string(body))
|
||||
if len(snip) > 220 {
|
||||
snip = snip[:220] + "…"
|
||||
}
|
||||
return Result{OK: false, Detail: fmt.Sprintf("HTTP %d %s", resp.StatusCode, snip), HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
}
|
||||
|
||||
func probeKiro(accessToken string) Result {
|
||||
arn := findProfileArnInJWT(accessToken)
|
||||
if arn == "" {
|
||||
return Result{
|
||||
OK: false,
|
||||
Detail: "无法从 Token 中解析 profileArn,Kiro 暂无法自动探测",
|
||||
}
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("origin", "AI_EDITOR")
|
||||
q.Set("profileArn", arn)
|
||||
q.Set("resourceType", "AGENTIC_REQUEST")
|
||||
u := "https://q.us-east-1.amazonaws.com/getUsageLimits?" + q.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: err.Error()}
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+normalizeBearerToken(accessToken))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: "请求失败: " + err.Error()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return Result{OK: true, Detail: "Kiro(AWS Q)用量接口响应正常", HTTPStatus: resp.StatusCode}
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return Result{OK: false, Detail: fmt.Sprintf("Token 无效或已过期(HTTP %d)", resp.StatusCode), HTTPStatus: resp.StatusCode}
|
||||
default:
|
||||
snip := strings.TrimSpace(string(body))
|
||||
if len(snip) > 220 {
|
||||
snip = snip[:220] + "…"
|
||||
}
|
||||
return Result{OK: false, Detail: fmt.Sprintf("HTTP %d %s", resp.StatusCode, snip), HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
|
||||
tok := normalizeBearerToken(strings.TrimSpace(raw))
|
||||
parts := strings.Split(tok, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("not a JWT")
|
||||
}
|
||||
b, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func findProfileArnInJWT(raw string) string {
|
||||
m, err := decodeJWTPayloadMap(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return findProfileArnValue(m)
|
||||
}
|
||||
|
||||
func findProfileArnValue(v interface{}) string {
|
||||
switch x := v.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, val := range x {
|
||||
lk := strings.ToLower(k)
|
||||
if lk == "profilearn" || lk == "profile_arn" {
|
||||
if s, ok := val.(string); ok && strings.Contains(s, "arn:") {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, val := range x {
|
||||
if s := findProfileArnValue(val); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, el := range x {
|
||||
if s := findProfileArnValue(el); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
case string:
|
||||
if strings.Contains(x, "arn:aws:codewhisperer") && strings.Contains(x, ":profile/") {
|
||||
return x
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -10,4 +10,14 @@ import (
|
||||
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")
|
||||
}
|
||||
|
||||
+118
-31
@@ -14,41 +14,128 @@ func Register() {
|
||||
|
||||
// RegisterAuthRoutes 注册 backend 认证相关路由。
|
||||
func RegisterAuthRoutes() {
|
||||
// backend 登录相关(统一走 /backend/*)
|
||||
beego.Router("/backend/login", &controllers.PlatformAuthController{}, "post:LoginBackend")
|
||||
beego.Router("/backend/sendLoginCode", &controllers.PlatformAuthController{}, "post:SendLoginCode")
|
||||
beego.Router("/backend/loginBySms", &controllers.PlatformAuthController{}, "post:LoginBySms")
|
||||
beego.Router("/backend/logout", &controllers.PlatformAuthController{}, "post:Logout")
|
||||
// 登录、注册与找回密码相关
|
||||
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/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/login/getGeetest3Infos", &controllers.PlatformAuthController{}, "get:GetGeetest3Infos")
|
||||
beego.Router("/backend/login/getGeetest4Infos", &controllers.PlatformAuthController{}, "get:GetGeetest4Infos")
|
||||
beego.Router("/backend/login/getOpenVerify", &controllers.PlatformAuthController{}, "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.PlatformAuthController{}, "post:Register")
|
||||
beego.Router("/backend/sendRegisterCode", &controllers.PlatformAuthController{}, "post:SendRegisterCode")
|
||||
beego.Router("/backend/resetPassword", &controllers.PlatformAuthController{}, "post:ResetPassword")
|
||||
beego.Router("/backend/sendResetCode", &controllers.PlatformAuthController{}, "post:SendResetCode")
|
||||
// 菜单接口
|
||||
beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu")
|
||||
beego.Router("/backend/allmenu", &controllers.BackendMenuController{}, "get:GetAllBackendMenus")
|
||||
|
||||
// backend 菜单相关(租户端菜单)
|
||||
beego.Router("/backend/menu/:id", &controllers.AdminMenuController{}, "get:GetBackendMenu")
|
||||
beego.Router("/backend/allmenu", &controllers.AdminMenuController{}, "get:GetAllBackendMenus")
|
||||
beego.Router("/backend/menu/status/:id", &controllers.AdminMenuController{}, "patch:UpdateMenuStatus")
|
||||
beego.Router("/backend/createmenu", &controllers.AdminMenuController{}, "post:CreateMenu")
|
||||
beego.Router("/backend/updatemenu/:id", &controllers.AdminMenuController{}, "put:UpdateMenu")
|
||||
beego.Router("/backend/deletemenu/:id", &controllers.AdminMenuController{}, "delete:DeleteMenu")
|
||||
// 操作日志(yz_system_operation_log)
|
||||
beego.Router("/backend/operationLogs", &controllers.BackendOperationLogController{}, "get:List")
|
||||
beego.Router("/backend/operationLogs/statistics", &controllers.BackendOperationLogController{}, "get:Statistics")
|
||||
beego.Router("/backend/operationLogs/:id", &controllers.BackendOperationLogController{}, "get:Detail;delete:Delete")
|
||||
beego.Router("/backend/operationLogs/batchDelete", &controllers.BackendOperationLogController{}, "post:BatchDelete")
|
||||
|
||||
// 模块管理(yz_system_modules)——语义更正:租户端走 /backend/modules/*
|
||||
beego.Router("/backend/modules/list", &controllers.PlatformModulesController{}, "get:GetList")
|
||||
beego.Router("/backend/modules/getTenantList", &controllers.PlatformModulesController{}, "get:GetTenantList")
|
||||
beego.Router("/backend/modules/select/list", &controllers.PlatformModulesController{}, "get:GetSelectList")
|
||||
beego.Router("/backend/modules/status", &controllers.PlatformModulesController{}, "post:ChangeStatus")
|
||||
beego.Router("/backend/modules/batchDelete", &controllers.PlatformModulesController{}, "post:BatchDelete")
|
||||
beego.Router("/backend/modules", &controllers.PlatformModulesController{}, "post:Add")
|
||||
beego.Router("/backend/modules/:id", &controllers.PlatformModulesController{}, "get:GetDetail;put:Edit;delete:Delete")
|
||||
// 租户站点设置
|
||||
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")
|
||||
beego.Router("/backend/catefiles/:id", &controllers.BackendFileController{}, "get:GetCateFiles")
|
||||
beego.Router("/backend/file/:id", &controllers.BackendFileController{}, "get:GetFileByID")
|
||||
beego.Router("/backend/deletefilepermanently/:id", &controllers.BackendFileController{}, "delete:DeleteFilePermanently")
|
||||
beego.Router("/backend/uploadfile", &controllers.BackendFileController{}, "post:UploadFile")
|
||||
beego.Router("/backend/uploadfiles", &controllers.BackendFileController{}, "post:UploadFile")
|
||||
beego.Router("/backend/updatefile/:id", &controllers.BackendFileController{}, "post:UpdateFile")
|
||||
beego.Router("/backend/deletefile/:id", &controllers.BackendFileController{}, "delete:DeleteFile")
|
||||
beego.Router("/backend/movefile/:id", &controllers.BackendFileController{}, "get:MoveFile")
|
||||
beego.Router("/backend/createfilecate", &controllers.BackendFileController{}, "post:CreateFileCate")
|
||||
beego.Router("/backend/renamefilecate/:id", &controllers.BackendFileController{}, "post:RenameFileCate")
|
||||
beego.Router("/backend/deletefilecate/:id", &controllers.BackendFileController{}, "delete:DeleteFileCate")
|
||||
beego.Router("/backend/uploadavatar", &controllers.BackendFileController{}, "post:UploadAvatar")
|
||||
beego.Router("/backend/uploadavatar/:id", &controllers.BackendFileController{}, "post:UpdateAvatar")
|
||||
beego.Router("/backend/batchdeletefiles", &controllers.BackendFileController{}, "post:BatchDeleteFiles")
|
||||
beego.Router("/backend/batchDeleteFilesPermanently", &controllers.BackendFileController{}, "post:BatchDeleteFilesPermanently")
|
||||
beego.Router("/backend/batchMoveFiles", &controllers.BackendFileController{}, "post:BatchMoveFiles")
|
||||
|
||||
// 模块接口
|
||||
beego.Router("/backend/modules/getTenantList", &controllers.BackendModulesController{}, "get:GetTenantList")
|
||||
|
||||
// 用户接口
|
||||
beego.Router("/backend/getTenantUsers/:tid", &controllers.BackendAdminUserController{}, "get:GetTenantUsers")
|
||||
beego.Router("/backend/getAllUsers", &controllers.BackendAdminUserController{}, "get:GetAllUsers")
|
||||
beego.Router("/backend/getUserInfo/:id", &controllers.BackendAdminUserController{}, "get:GetUserInfo")
|
||||
beego.Router("/backend/addUser", &controllers.BackendAdminUserController{}, "post:AddUser")
|
||||
beego.Router("/backend/editUser/:id", &controllers.BackendAdminUserController{}, "post:EditUser")
|
||||
beego.Router("/backend/deleteUser/:id", &controllers.BackendAdminUserController{}, "delete:DeleteUser")
|
||||
beego.Router("/backend/changePassword", &controllers.BackendAdminUserController{}, "post:ChangePassword")
|
||||
|
||||
// ERP 接口
|
||||
beego.Router("/backend/erp/getOrganization", &controllers.BackendErpController{}, "get:GetOrganization")
|
||||
beego.Router("/backend/erp/getOrganizationDetail/:id", &controllers.BackendErpController{}, "get:GetOrganizationDetail")
|
||||
beego.Router("/backend/erp/createOrganization", &controllers.BackendErpController{}, "post:CreateOrganization")
|
||||
beego.Router("/backend/erp/editOrganization/:id", &controllers.BackendErpController{}, "post:EditOrganization")
|
||||
beego.Router("/backend/erp/deleteOrganization/:id", &controllers.BackendErpController{}, "delete:DeleteOrganization")
|
||||
beego.Router("/backend/erp/getCompanys", &controllers.BackendErpController{}, "get:GetCompanys")
|
||||
beego.Router("/backend/erp/getDepartments", &controllers.BackendErpController{}, "get:GetDepartments")
|
||||
beego.Router("/backend/erp/getEmployee", &controllers.BackendErpController{}, "get:GetEmployee")
|
||||
beego.Router("/backend/erp/getEmployeeDetail/:id", &controllers.BackendErpController{}, "get:GetEmployeeDetail")
|
||||
beego.Router("/backend/erp/createEmployee", &controllers.BackendErpController{}, "post:CreateEmployee")
|
||||
beego.Router("/backend/erp/editEmployee/:id", &controllers.BackendErpController{}, "post:EditEmployee")
|
||||
beego.Router("/backend/erp/deleteEmployee/:id", &controllers.BackendErpController{}, "delete:DeleteEmployee")
|
||||
beego.Router("/backend/erp/getPosition", &controllers.BackendErpController{}, "get:GetPosition")
|
||||
beego.Router("/backend/erp/getPositionDetail/:id", &controllers.BackendErpController{}, "get:GetPositionDetail")
|
||||
beego.Router("/backend/erp/createPosition", &controllers.BackendErpController{}, "post:CreatePosition")
|
||||
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")
|
||||
|
||||
// 租户站点设置(yz_tenant_site_setting)
|
||||
beego.Router("/backend/normalInfos", &controllers.SiteSettingsController{}, "get:GetNormalInfos")
|
||||
beego.Router("/backend/saveNormalInfos", &controllers.SiteSettingsController{}, "post:SaveNormalInfos")
|
||||
}
|
||||
|
||||
@@ -22,6 +22,14 @@ func Register() {
|
||||
beego.Router("/platform/loginVerifyInfos", &controllers.PlatformLoginVerifyController{}, "get:GetLoginVerifyInfos")
|
||||
beego.Router("/platform/saveloginVerifyInfos", &controllers.PlatformLoginVerifyController{}, "post:SaveLoginVerifyInfos")
|
||||
|
||||
// 存储配置
|
||||
beego.Router("/platform/storageConfig", &controllers.StorageConfigController{}, "get:GetStorageConfig")
|
||||
beego.Router("/platform/saveStorageConfig", &controllers.StorageConfigController{}, "post:SaveStorageConfig")
|
||||
|
||||
// 存储迁移
|
||||
beego.Router("/platform/storage/migrateToQiniu", &controllers.StorageMigrationController{}, "post:MigrateToQiniu")
|
||||
beego.Router("/platform/storage/migrationProgress", &controllers.StorageMigrationController{}, "get:GetMigrationProgress")
|
||||
|
||||
// 找回密码相关
|
||||
beego.Router("/platform/resetPassword", &controllers.PlatformAuthController{}, "post:ResetPassword")
|
||||
beego.Router("/platform/sendResetCode", &controllers.PlatformAuthController{}, "post:SendResetCode")
|
||||
@@ -110,8 +118,8 @@ func Register() {
|
||||
beego.Router("/platform/softwareupgrade/:id", &controllers.PlatformSoftwareUpgradeController{}, "get:Detail;post:Update;delete:Delete")
|
||||
|
||||
// 租户站点设置(yz_tenant_site_setting)
|
||||
beego.Router("/platform/normalInfos", &controllers.SiteSettingsController{}, "get:GetNormalInfos")
|
||||
beego.Router("/platform/saveNormalInfos", &controllers.SiteSettingsController{}, "post:SaveNormalInfos")
|
||||
beego.Router("/platform/normalInfos", &controllers.PlatformSiteSettingsController{}, "get:GetNormalInfos")
|
||||
beego.Router("/platform/saveNormalInfos", &controllers.PlatformSiteSettingsController{}, "post:SaveNormalInfos")
|
||||
|
||||
// 系统邮箱配置(yz_system_email)
|
||||
beego.Router("/platform/email/info", &controllers.PlatformEmailController{}, "get:GetInfo")
|
||||
@@ -144,4 +152,72 @@ func Register() {
|
||||
beego.Router("/platform/batchdeletefiles", &controllers.PlatformFileController{}, "post:BatchDeleteFiles")
|
||||
beego.Router("/platform/batchDeleteFilesPermanently", &controllers.PlatformFileController{}, "post:BatchDeleteFilesPermanently")
|
||||
beego.Router("/platform/batchMoveFiles", &controllers.PlatformFileController{}, "post:BatchMoveFiles")
|
||||
|
||||
// 七牛云直传相关
|
||||
beego.Router("/platform/storage/config", &controllers.QiniuUploadController{}, "get:GetStorageConfig")
|
||||
beego.Router("/platform/qiniu/token", &controllers.QiniuUploadController{}, "get:GetUploadToken")
|
||||
beego.Router("/platform/qiniu/save", &controllers.QiniuUploadController{}, "post:SaveFileRecord")
|
||||
|
||||
// 首页统计
|
||||
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")
|
||||
beego.Router("/platform/accountPool/cursor/batchAdd", &controllers.PlatformAccountPoolCursorController{}, "post:BatchAdd")
|
||||
beego.Router("/platform/accountPool/cursor/detail/:id", &controllers.PlatformAccountPoolCursorController{}, "get:Detail")
|
||||
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")
|
||||
beego.Router("/platform/accountPool/cursor/probeToken", &controllers.PlatformAccountPoolCursorController{}, "post:ProbeToken")
|
||||
|
||||
beego.Router("/platform/accountPool/windsurf/list", &controllers.PlatformAccountPoolWindsurfController{}, "get:List")
|
||||
beego.Router("/platform/accountPool/windsurf/add", &controllers.PlatformAccountPoolWindsurfController{}, "post:Add")
|
||||
beego.Router("/platform/accountPool/windsurf/batchAdd", &controllers.PlatformAccountPoolWindsurfController{}, "post:BatchAdd")
|
||||
beego.Router("/platform/accountPool/windsurf/detail/:id", &controllers.PlatformAccountPoolWindsurfController{}, "get:Detail")
|
||||
beego.Router("/platform/accountPool/windsurf/extract", &controllers.PlatformAccountPoolWindsurfController{}, "post:Extract")
|
||||
beego.Router("/platform/accountPool/windsurf/updateRemark", &controllers.PlatformAccountPoolWindsurfController{}, "post:UpdateRemark")
|
||||
beego.Router("/platform/accountPool/windsurf/setUnavailable", &controllers.PlatformAccountPoolWindsurfController{}, "post:SetUnavailable")
|
||||
beego.Router("/platform/accountPool/windsurf/updatePlatform", &controllers.PlatformAccountPoolWindsurfController{}, "post:UpdatePlatform")
|
||||
beego.Router("/platform/accountPool/windsurf/unextract", &controllers.PlatformAccountPoolWindsurfController{}, "post:Unextract")
|
||||
beego.Router("/platform/accountPool/windsurf/replenish", &controllers.PlatformAccountPoolWindsurfController{}, "post:Replenish")
|
||||
beego.Router("/platform/accountPool/windsurf/probeToken", &controllers.PlatformAccountPoolWindsurfController{}, "post:ProbeToken")
|
||||
|
||||
beego.Router("/platform/accountPool/krio/list", &controllers.PlatformAccountPoolKrioController{}, "get:List")
|
||||
beego.Router("/platform/accountPool/krio/add", &controllers.PlatformAccountPoolKrioController{}, "post:Add")
|
||||
beego.Router("/platform/accountPool/krio/batchAdd", &controllers.PlatformAccountPoolKrioController{}, "post:BatchAdd")
|
||||
beego.Router("/platform/accountPool/krio/detail/:id", &controllers.PlatformAccountPoolKrioController{}, "get:Detail")
|
||||
beego.Router("/platform/accountPool/krio/extract", &controllers.PlatformAccountPoolKrioController{}, "post:Extract")
|
||||
beego.Router("/platform/accountPool/krio/updateRemark", &controllers.PlatformAccountPoolKrioController{}, "post:UpdateRemark")
|
||||
beego.Router("/platform/accountPool/krio/setUnavailable", &controllers.PlatformAccountPoolKrioController{}, "post:SetUnavailable")
|
||||
beego.Router("/platform/accountPool/krio/updatePlatform", &controllers.PlatformAccountPoolKrioController{}, "post:UpdatePlatform")
|
||||
beego.Router("/platform/accountPool/krio/unextract", &controllers.PlatformAccountPoolKrioController{}, "post:Unextract")
|
||||
beego.Router("/platform/accountPool/krio/replenish", &controllers.PlatformAccountPoolKrioController{}, "post:Replenish")
|
||||
beego.Router("/platform/accountPool/krio/probeToken", &controllers.PlatformAccountPoolKrioController{}, "post:ProbeToken")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[Unit]
|
||||
Description=Go API Server
|
||||
After=network.target mysql.service
|
||||
Wants=mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/www/wwwroot/api.yunzer.cn
|
||||
ExecStart=/usr/local/go/bin/go run main.go
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=append:/www/wwwroot/api.yunzer.cn/go.log
|
||||
StandardError=append:/www/wwwroot/api.yunzer.cn/go.log
|
||||
|
||||
# 环境变量
|
||||
Environment="PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
# 资源限制
|
||||
LimitNOFILE=65535
|
||||
LimitNPROC=65535
|
||||
|
||||
# 安全设置
|
||||
PrivateTmp=true
|
||||
NoNewPrivileges=false
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ========================================
|
||||
# 安装 systemd 服务脚本
|
||||
# 用途:配置 Go API 为 systemd 服务
|
||||
# ========================================
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 配置
|
||||
SERVICE_NAME="go-api"
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SERVICE_TEMPLATE="${SCRIPT_DIR}/${SERVICE_NAME}.service"
|
||||
WORK_DIR="/www/wwwroot/api.yunzer.cn"
|
||||
MAIN_FILE="${WORK_DIR}/main.go"
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}安装 Go API systemd 服务${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# 检查是否为 root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}错误:请使用 root 用户运行此脚本${NC}"
|
||||
echo "使用方法: sudo bash $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查工作目录
|
||||
if [ ! -d "$WORK_DIR" ]; then
|
||||
echo -e "${RED}错误:工作目录不存在: $WORK_DIR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 main.go
|
||||
if [ ! -f "$MAIN_FILE" ]; then
|
||||
echo -e "${RED}错误:找不到 main.go: $MAIN_FILE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 Go 是否安装
|
||||
if ! command -v go &> /dev/null; then
|
||||
echo -e "${RED}错误:Go 未安装或不在 PATH 中${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GO_PATH=$(which go)
|
||||
echo -e "${GREEN}✓ Go 路径: $GO_PATH${NC}"
|
||||
|
||||
# 停止现有服务
|
||||
echo -e "${YELLOW}1. 停止现有服务...${NC}"
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
systemctl stop "$SERVICE_NAME"
|
||||
echo -e "${GREEN} ✓ 已停止现有服务${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} - 服务未运行${NC}"
|
||||
fi
|
||||
|
||||
# 停止可能的手动启动进程
|
||||
echo -e "${YELLOW}2. 清理手动启动的进程...${NC}"
|
||||
if pgrep -f "go run main.go" > /dev/null; then
|
||||
pkill -f "go run main.go"
|
||||
echo -e "${GREEN} ✓ 已清理手动启动的进程${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} - 无手动启动的进程${NC}"
|
||||
fi
|
||||
|
||||
# 创建服务文件
|
||||
echo -e "${YELLOW}3. 创建 systemd 服务文件...${NC}"
|
||||
|
||||
cat > "$SERVICE_FILE" << EOF
|
||||
[Unit]
|
||||
Description=Go API Server
|
||||
After=network.target mysql.service
|
||||
Wants=mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=$WORK_DIR
|
||||
ExecStart=$GO_PATH run main.go
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=append:$WORK_DIR/go.log
|
||||
StandardError=append:$WORK_DIR/go.log
|
||||
|
||||
# 环境变量
|
||||
Environment="PATH=$GO_PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
# 资源限制
|
||||
LimitNOFILE=65535
|
||||
LimitNPROC=65535
|
||||
|
||||
# 安全设置
|
||||
PrivateTmp=true
|
||||
NoNewPrivileges=false
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN} ✓ 服务文件已创建: $SERVICE_FILE${NC}"
|
||||
|
||||
# 重载 systemd
|
||||
echo -e "${YELLOW}4. 重载 systemd 配置...${NC}"
|
||||
systemctl daemon-reload
|
||||
echo -e "${GREEN} ✓ systemd 配置已重载${NC}"
|
||||
|
||||
# 启动服务
|
||||
echo -e "${YELLOW}5. 启动服务...${NC}"
|
||||
systemctl start "$SERVICE_NAME"
|
||||
sleep 2
|
||||
|
||||
# 检查服务状态
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
echo -e "${GREEN} ✓ 服务启动成功${NC}"
|
||||
else
|
||||
echo -e "${RED} ✗ 服务启动失败${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}查看错误日志:${NC}"
|
||||
journalctl -u "$SERVICE_NAME" -n 20 --no-pager
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 启用开机自启
|
||||
echo -e "${YELLOW}6. 启用开机自启...${NC}"
|
||||
systemctl enable "$SERVICE_NAME"
|
||||
echo -e "${GREEN} ✓ 已启用开机自启${NC}"
|
||||
|
||||
# 显示服务状态
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}安装完成!${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}服务信息:${NC}"
|
||||
echo -e " 服务名称: $SERVICE_NAME"
|
||||
echo -e " 工作目录: $WORK_DIR"
|
||||
echo -e " 日志文件: $WORK_DIR/go.log"
|
||||
echo -e " 配置文件: $SERVICE_FILE"
|
||||
echo ""
|
||||
echo -e "${GREEN}常用命令:${NC}"
|
||||
echo -e " 启动服务: systemctl start $SERVICE_NAME"
|
||||
echo -e " 停止服务: systemctl stop $SERVICE_NAME"
|
||||
echo -e " 重启服务: systemctl restart $SERVICE_NAME"
|
||||
echo -e " 查看状态: systemctl status $SERVICE_NAME"
|
||||
echo -e " 查看日志: journalctl -u $SERVICE_NAME -f"
|
||||
echo -e " 查看文件日志: tail -f $WORK_DIR/go.log"
|
||||
echo ""
|
||||
echo -e "${YELLOW}当前服务状态:${NC}"
|
||||
systemctl status "$SERVICE_NAME" --no-pager -l
|
||||
echo ""
|
||||
echo -e "${YELLOW}最近日志(最后 10 行):${NC}"
|
||||
if [ -f "$WORK_DIR/go.log" ]; then
|
||||
tail -n 10 "$WORK_DIR/go.log"
|
||||
else
|
||||
echo " 日志文件尚未创建"
|
||||
fi
|
||||
echo ""
|
||||
@@ -0,0 +1,36 @@
|
||||
@echo off
|
||||
REM 安装Go依赖脚本 (Windows)
|
||||
|
||||
echo 开始安装Go依赖...
|
||||
echo.
|
||||
|
||||
REM 进入go目录
|
||||
cd /d "%~dp0\.."
|
||||
|
||||
REM 下载依赖
|
||||
echo 下载依赖包...
|
||||
go mod download
|
||||
|
||||
REM 整理依赖
|
||||
echo 整理依赖...
|
||||
go mod tidy
|
||||
|
||||
REM 验证依赖
|
||||
echo 验证依赖...
|
||||
go mod verify
|
||||
|
||||
echo.
|
||||
echo 依赖安装完成!
|
||||
echo.
|
||||
echo 已安装的主要依赖:
|
||||
echo - github.com/beego/beego/v2
|
||||
echo - github.com/qiniu/go-sdk/v7 (七牛云SDK)
|
||||
echo - github.com/golang-jwt/jwt/v5
|
||||
echo - github.com/go-sql-driver/mysql
|
||||
echo.
|
||||
echo 下一步:
|
||||
echo 1. 执行数据库迁移: mysql -u root -p your_database ^< migrations/add_storage_config_table.sql
|
||||
echo 2. 配置存储设置: 访问平台管理后台 -^> 系统设置 -^> 平台设置 -^> 存储配置
|
||||
echo 3. 重启服务: bee run 或 go run main.go
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 安装Go依赖脚本
|
||||
|
||||
echo "开始安装Go依赖..."
|
||||
|
||||
# 进入go目录
|
||||
cd "$(dirname "$0")/.." || exit
|
||||
|
||||
# 下载依赖
|
||||
echo "下载依赖包..."
|
||||
go mod download
|
||||
|
||||
# 整理依赖
|
||||
echo "整理依赖..."
|
||||
go mod tidy
|
||||
|
||||
# 验证依赖
|
||||
echo "验证依赖..."
|
||||
go mod verify
|
||||
|
||||
echo "依赖安装完成!"
|
||||
echo ""
|
||||
echo "已安装的主要依赖:"
|
||||
echo "- github.com/beego/beego/v2"
|
||||
echo "- github.com/qiniu/go-sdk/v7 (七牛云SDK)"
|
||||
echo "- github.com/golang-jwt/jwt/v5"
|
||||
echo "- github.com/go-sql-driver/mysql"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo "1. 执行数据库迁移: mysql -u root -p your_database < migrations/add_storage_config_table.sql"
|
||||
echo "2. 配置存储设置: 访问平台管理后台 -> 系统设置 -> 平台设置 -> 存储配置"
|
||||
echo "3. 重启服务: bee run 或 go run main.go"
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 快速重启脚本
|
||||
echo "停止服务..."
|
||||
systemctl stop go-api
|
||||
pkill -f "go run main.go"
|
||||
|
||||
echo "启动服务..."
|
||||
systemctl start go-api
|
||||
|
||||
echo "等待服务启动..."
|
||||
sleep 3
|
||||
|
||||
echo "查看服务状态..."
|
||||
systemctl status go-api --no-pager
|
||||
|
||||
echo ""
|
||||
echo "查看最近日志..."
|
||||
tail -n 20 /www/wwwroot/api.yunzer.cn/go.log
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ========================================
|
||||
# Go 服务管理脚本
|
||||
# 用途:启动、停止、重启、查看状态
|
||||
# ========================================
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 配置
|
||||
SERVICE_DIR="/www/wwwroot/api.yunzer.cn"
|
||||
LOG_FILE="$SERVICE_DIR/go.log"
|
||||
PID_FILE="$SERVICE_DIR/go.pid"
|
||||
MAIN_FILE="$SERVICE_DIR/main.go"
|
||||
|
||||
# 检查服务目录
|
||||
if [ ! -d "$SERVICE_DIR" ]; then
|
||||
echo -e "${RED}错误:服务目录不存在: $SERVICE_DIR${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 main.go
|
||||
if [ ! -f "$MAIN_FILE" ]; then
|
||||
echo -e "${RED}错误:找不到 main.go: $MAIN_FILE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 获取进程 ID
|
||||
get_pid() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
cat "$PID_FILE"
|
||||
else
|
||||
# 通过进程名查找
|
||||
pgrep -f "go run main.go" | head -n 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查服务是否运行
|
||||
is_running() {
|
||||
local pid=$(get_pid)
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动服务
|
||||
start() {
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}启动 Go 服务${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
if is_running; then
|
||||
local pid=$(get_pid)
|
||||
echo -e "${YELLOW}服务已在运行中 (PID: $pid)${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}切换到服务目录...${NC}"
|
||||
cd "$SERVICE_DIR"
|
||||
|
||||
echo -e "${YELLOW}启动服务...${NC}"
|
||||
nohup go run main.go > "$LOG_FILE" 2>&1 &
|
||||
local pid=$!
|
||||
echo $pid > "$PID_FILE"
|
||||
|
||||
# 等待服务启动
|
||||
sleep 2
|
||||
|
||||
if is_running; then
|
||||
echo -e "${GREEN}✓ 服务启动成功 (PID: $pid)${NC}"
|
||||
echo -e "${GREEN}✓ 日志文件: $LOG_FILE${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}查看日志:${NC}"
|
||||
echo -e " tail -f $LOG_FILE"
|
||||
echo ""
|
||||
echo -e "${YELLOW}查看最近日志:${NC}"
|
||||
tail -n 20 "$LOG_FILE"
|
||||
else
|
||||
echo -e "${RED}✗ 服务启动失败${NC}"
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo -e "${YELLOW}最近的错误日志:${NC}"
|
||||
tail -n 20 "$LOG_FILE"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 停止服务
|
||||
stop() {
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}停止 Go 服务${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
if ! is_running; then
|
||||
echo -e "${YELLOW}服务未运行${NC}"
|
||||
rm -f "$PID_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local pid=$(get_pid)
|
||||
echo -e "${YELLOW}停止服务 (PID: $pid)...${NC}"
|
||||
|
||||
# 尝试优雅停止
|
||||
kill "$pid" 2>/dev/null || true
|
||||
|
||||
# 等待最多 10 秒
|
||||
local count=0
|
||||
while is_running && [ $count -lt 10 ]; do
|
||||
sleep 1
|
||||
count=$((count + 1))
|
||||
echo -n "."
|
||||
done
|
||||
echo ""
|
||||
|
||||
# 如果还在运行,强制停止
|
||||
if is_running; then
|
||||
echo -e "${YELLOW}强制停止服务...${NC}"
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# 清理所有相关进程
|
||||
pkill -f "go run main.go" 2>/dev/null || true
|
||||
|
||||
rm -f "$PID_FILE"
|
||||
|
||||
if is_running; then
|
||||
echo -e "${RED}✗ 服务停止失败${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✓ 服务已停止${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 重启服务
|
||||
restart() {
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}重启 Go 服务${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
stop
|
||||
echo ""
|
||||
sleep 2
|
||||
start
|
||||
}
|
||||
|
||||
# 查看状态
|
||||
status() {
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Go 服务状态${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
if is_running; then
|
||||
local pid=$(get_pid)
|
||||
echo -e "${GREEN}✓ 服务运行中${NC}"
|
||||
echo -e " PID: $pid"
|
||||
echo -e " 目录: $SERVICE_DIR"
|
||||
echo -e " 日志: $LOG_FILE"
|
||||
echo ""
|
||||
|
||||
# 显示进程信息
|
||||
echo -e "${YELLOW}进程信息:${NC}"
|
||||
ps aux | grep "$pid" | grep -v grep
|
||||
echo ""
|
||||
|
||||
# 显示端口监听
|
||||
echo -e "${YELLOW}端口监听:${NC}"
|
||||
netstat -tlnp 2>/dev/null | grep "$pid" || lsof -i -P -n | grep "$pid" || echo " 无法获取端口信息"
|
||||
echo ""
|
||||
|
||||
# 显示最近日志
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo -e "${YELLOW}最近日志(最后 10 行):${NC}"
|
||||
tail -n 10 "$LOG_FILE"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ 服务未运行${NC}"
|
||||
|
||||
# 检查是否有残留进程
|
||||
local pids=$(pgrep -f "go run main.go" || true)
|
||||
if [ -n "$pids" ]; then
|
||||
echo -e "${YELLOW}发现残留进程:${NC}"
|
||||
ps aux | grep "go run main.go" | grep -v grep
|
||||
echo ""
|
||||
echo -e "${YELLOW}清理残留进程:${NC}"
|
||||
echo " bash $0 stop"
|
||||
fi
|
||||
|
||||
# 显示最近日志
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}最近日志(最后 20 行):${NC}"
|
||||
tail -n 20 "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 查看日志
|
||||
logs() {
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
echo -e "${RED}日志文件不存在: $LOG_FILE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$1" = "-f" ] || [ "$1" = "--follow" ]; then
|
||||
echo -e "${YELLOW}实时查看日志(Ctrl+C 退出):${NC}"
|
||||
tail -f "$LOG_FILE"
|
||||
else
|
||||
local lines=${1:-50}
|
||||
echo -e "${YELLOW}最近 $lines 行日志:${NC}"
|
||||
tail -n "$lines" "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示帮助
|
||||
help() {
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Go 服务管理脚本${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo "用法: $0 {start|stop|restart|status|logs}"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " start - 启动服务"
|
||||
echo " stop - 停止服务"
|
||||
echo " restart - 重启服务"
|
||||
echo " status - 查看服务状态"
|
||||
echo " logs - 查看日志(默认最后 50 行)"
|
||||
echo " logs -f - 实时查看日志"
|
||||
echo " logs 100 - 查看最后 100 行日志"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 start # 启动服务"
|
||||
echo " $0 restart # 重启服务"
|
||||
echo " $0 status # 查看状态"
|
||||
echo " $0 logs -f # 实时查看日志"
|
||||
echo " $0 logs 100 # 查看最后 100 行"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
logs)
|
||||
logs "${2:-}"
|
||||
;;
|
||||
help|--help|-h)
|
||||
help
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}错误:未知命令 '$1'${NC}"
|
||||
echo ""
|
||||
help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 运行主函数
|
||||
main "$@"
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 存储功能测试脚本
|
||||
|
||||
echo "================================"
|
||||
echo "存储功能测试"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
# 颜色定义
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 测试结果
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# 测试函数
|
||||
test_api() {
|
||||
local name=$1
|
||||
local method=$2
|
||||
local url=$3
|
||||
local data=$4
|
||||
|
||||
echo -n "测试 $name ... "
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response=$(curl -s -w "\n%{http_code}" "$url")
|
||||
else
|
||||
response=$(curl -s -w "\n%{http_code}" -X "$method" -H "Content-Type: application/json" -d "$data" "$url")
|
||||
fi
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | head -n-1)
|
||||
|
||||
if [ "$http_code" = "200" ] || [ "$http_code" = "201" ]; then
|
||||
echo -e "${GREEN}✓ PASS${NC}"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo -e "${RED}✗ FAIL${NC} (HTTP $http_code)"
|
||||
echo " 响应: $body"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# 基础URL
|
||||
BASE_URL="http://localhost:8080"
|
||||
|
||||
echo "1. 测试存储配置API"
|
||||
echo "-------------------"
|
||||
|
||||
# 测试获取存储配置
|
||||
test_api "获取存储配置" "GET" "$BASE_URL/platform/storageConfig"
|
||||
|
||||
# 测试保存本地存储配置
|
||||
test_api "保存本地存储配置" "POST" "$BASE_URL/platform/saveStorageConfig" \
|
||||
'{"storage_type":"local"}'
|
||||
|
||||
echo ""
|
||||
echo "2. 测试文件上传"
|
||||
echo "-------------------"
|
||||
|
||||
# 创建测试文件
|
||||
TEST_FILE="/tmp/test_upload.txt"
|
||||
echo "This is a test file" > "$TEST_FILE"
|
||||
|
||||
# 测试文件上传(需要认证token,这里简化)
|
||||
echo -e "${YELLOW}注意: 文件上传需要认证token,请手动测试${NC}"
|
||||
|
||||
echo ""
|
||||
echo "3. 检查数据库表"
|
||||
echo "-------------------"
|
||||
|
||||
# 检查数据库表是否存在(需要MySQL连接信息)
|
||||
echo -e "${YELLOW}请手动检查数据库表: yz_system_storage_config${NC}"
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "测试结果"
|
||||
echo "================================"
|
||||
echo -e "通过: ${GREEN}$PASS${NC}"
|
||||
echo -e "失败: ${RED}$FAIL${NC}"
|
||||
echo ""
|
||||
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo -e "${GREEN}所有测试通过!${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}部分测试失败,请检查日志${NC}"
|
||||
exit 1
|
||||
fi
|
||||
BIN
Binary file not shown.
@@ -58,7 +58,6 @@ func SendPlatformLoginCode(account, channel string) error {
|
||||
Channel: channel,
|
||||
ExpiredAt: time.Now().Add(5 * time.Minute),
|
||||
})
|
||||
// TODO: 接入短信/邮箱发送通道。当前阶段只做服务端验证码校验链路。
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -98,19 +97,20 @@ func SendBackendLoginCode(tenantName, account, channel string) error {
|
||||
if channel != "sms" && channel != "email" {
|
||||
return errors.New("仅支持短信或邮箱验证码")
|
||||
}
|
||||
var tenant models.Tenant
|
||||
if err := models.Orm.QueryTable(new(models.Tenant)).Filter("tenant_name", tenantName).One(&tenant); err != nil {
|
||||
|
||||
var tenant models.SystemTenant
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_name", tenantName).One(&tenant); err != nil {
|
||||
return errors.New("租户不存在")
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||
|
||||
// 规则:先校验租户,再校验“输入的手机号/邮箱”是否为该租户已绑定的记录
|
||||
switch channel {
|
||||
case "sms":
|
||||
phone := account
|
||||
var user models.TenantUser
|
||||
if err := models.Orm.QueryTable(new(models.TenantUser)).
|
||||
var user models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tenant.ID).
|
||||
Filter("phone", phone).
|
||||
One(&user); err != nil {
|
||||
@@ -129,8 +129,8 @@ func SendBackendLoginCode(tenantName, account, channel string) error {
|
||||
}
|
||||
case "email":
|
||||
email := account
|
||||
var user models.TenantUser
|
||||
if err := models.Orm.QueryTable(new(models.TenantUser)).
|
||||
var user models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tenant.ID).
|
||||
Filter("email", email).
|
||||
One(&user); err != nil {
|
||||
@@ -165,7 +165,6 @@ func getDefaultSystemSMSConfig() (backendURL string, apiKey string, err error) {
|
||||
Limit(1).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
// fallback:自定义网关
|
||||
err2 := models.Orm.QueryTable(new(models.SystemSMS)).
|
||||
Filter("config_code", "custom").
|
||||
OrderBy("-id").
|
||||
@@ -219,7 +218,6 @@ func enqueueSMSTaskForLogin(tid uint64, phone, content, code string) error {
|
||||
return fmt.Errorf("gateway http status: %d, body: %s", resp.StatusCode, bodyStr)
|
||||
}
|
||||
|
||||
// 2xx:认为已成功提交
|
||||
now := time.Now()
|
||||
tidCopy := tid
|
||||
contentPtr := content
|
||||
@@ -241,10 +239,8 @@ func enqueueSMSTaskForLogin(tid uint64, phone, content, code string) error {
|
||||
}
|
||||
|
||||
_, insertErr := models.Orm.Insert(task)
|
||||
// 入队成功但写任务表失败:不影响用户侧体验
|
||||
if insertErr != nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func toPlatformLoginUser(user *models.AdminUser) *PlatformLoginUser {
|
||||
}
|
||||
}
|
||||
|
||||
// PlatformAdminLogin 平台端登录:仅校验 yz_system_admin_user(不需要租户)
|
||||
// PlatformAdminLogin 平台端登录:仅校验 yz_system_admin_user,不需要租户。
|
||||
func PlatformAdminLogin(account, password string) (string, *PlatformLoginUser, error) {
|
||||
account = strings.TrimSpace(account)
|
||||
password = strings.TrimSpace(password)
|
||||
@@ -84,7 +84,7 @@ func PlatformAdminLogin(account, password string) (string, *PlatformLoginUser, e
|
||||
return token, loginUser, nil
|
||||
}
|
||||
|
||||
// BackendLogin backend 登录:先校验租户,再校验租户下用户
|
||||
// BackendLogin backend 登录:先校验租户,再校验租户下用户账号和密码。
|
||||
func BackendLogin(tenantName, account, password string) (string, *PlatformLoginUser, error) {
|
||||
tenantName = strings.TrimSpace(tenantName)
|
||||
account = strings.TrimSpace(account)
|
||||
@@ -93,9 +93,8 @@ func BackendLogin(tenantName, account, password string) (string, *PlatformLoginU
|
||||
return "", nil, errors.New("租户名称、用户名或密码不能为空")
|
||||
}
|
||||
|
||||
// 1) 校验租户名称
|
||||
var tenant models.Tenant
|
||||
err := models.Orm.QueryTable(new(models.Tenant)).
|
||||
var tenant models.SystemTenant
|
||||
err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("tenant_name", tenantName).
|
||||
One(&tenant)
|
||||
if err != nil {
|
||||
@@ -105,9 +104,8 @@ func BackendLogin(tenantName, account, password string) (string, *PlatformLoginU
|
||||
return "", nil, errors.New("租户已停用")
|
||||
}
|
||||
|
||||
// 2) 在 tid 下校验租户用户账号和密码
|
||||
var tenantUser models.TenantUser
|
||||
err = models.Orm.QueryTable(new(models.TenantUser)).
|
||||
var tenantUser models.SystemTenantUser
|
||||
err = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tenant.ID).
|
||||
Filter("account", account).
|
||||
One(&tenantUser)
|
||||
@@ -143,10 +141,11 @@ func BackendLogin(tenantName, account, password string) (string, *PlatformLoginU
|
||||
if tenantUser.Name != nil {
|
||||
loginUser.Name = strings.TrimSpace(*tenantUser.Name)
|
||||
}
|
||||
|
||||
return token, loginUser, nil
|
||||
}
|
||||
|
||||
// PlatformGetCurrentUser 根据平台管理员用户 ID 返回登录用户信息(含角色名称)
|
||||
// PlatformGetCurrentUser 根据平台管理员用户 ID 返回登录用户信息(含角色名称)。
|
||||
func PlatformGetCurrentUser(uid uint64) (*PlatformLoginUser, error) {
|
||||
u, err := GetAdminUserByID(uid)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// MigrationProgress 迁移进度
|
||||
type MigrationProgress struct {
|
||||
Total int
|
||||
Success int
|
||||
Failed int
|
||||
Current string
|
||||
Errors []string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// AddSuccess 增加成功计数
|
||||
func (p *MigrationProgress) AddSuccess() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.Success++
|
||||
}
|
||||
|
||||
// AddFailed 增加失败计数
|
||||
func (p *MigrationProgress) AddFailed(err string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.Failed++
|
||||
p.Errors = append(p.Errors, err)
|
||||
}
|
||||
|
||||
// SetCurrent 设置当前处理的文件
|
||||
func (p *MigrationProgress) SetCurrent(filename string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.Current = filename
|
||||
}
|
||||
|
||||
// GetProgress 获取进度信息
|
||||
func (p *MigrationProgress) GetProgress() (int, int, int, string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.Total, p.Success, p.Failed, p.Current
|
||||
}
|
||||
|
||||
// StorageMigration 存储迁移服务
|
||||
type StorageMigration struct {
|
||||
fromService StorageService
|
||||
toService StorageService
|
||||
progress *MigrationProgress
|
||||
}
|
||||
|
||||
// NewStorageMigration 创建存储迁移服务
|
||||
func NewStorageMigration(from, to StorageService) *StorageMigration {
|
||||
return &StorageMigration{
|
||||
fromService: from,
|
||||
toService: to,
|
||||
progress: &MigrationProgress{
|
||||
Errors: make([]string, 0),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MigrateFile 迁移单个文件
|
||||
func (m *StorageMigration) MigrateFile(file *models.SystemFile) error {
|
||||
m.progress.SetCurrent(file.Name)
|
||||
|
||||
// 如果是本地存储,从本地读取文件
|
||||
if localFrom, ok := m.fromService.(*LocalStorage); ok {
|
||||
// 从本地文件系统读取
|
||||
localPath := strings.TrimPrefix(file.Src, "/")
|
||||
filePath := filepath.Join(localFrom.BaseDir, localPath)
|
||||
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开本地文件失败: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// 获取文件信息
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取文件信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建 multipart.FileHeader
|
||||
header := &multipart.FileHeader{
|
||||
Filename: file.Name,
|
||||
Size: stat.Size(),
|
||||
}
|
||||
|
||||
// 上传到目标存储
|
||||
result, err := m.toService.Upload(f, header)
|
||||
if err != nil {
|
||||
return fmt.Errorf("上传到目标存储失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新数据库记录
|
||||
_, err = models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id", file.ID).
|
||||
Update(map[string]interface{}{
|
||||
"src": result.URL,
|
||||
})
|
||||
if err != nil {
|
||||
// 上传成功但更新数据库失败,尝试删除已上传的文件
|
||||
_ = m.toService.Delete(result.Key)
|
||||
return fmt.Errorf("更新数据库失败: %w", err)
|
||||
}
|
||||
|
||||
m.progress.AddSuccess()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果是七牛云存储,需要先下载再上传(这里简化处理)
|
||||
return fmt.Errorf("暂不支持从七牛云迁移到本地")
|
||||
}
|
||||
|
||||
// MigrateAll 迁移所有文件
|
||||
func (m *StorageMigration) MigrateAll(tid uint64) error {
|
||||
// 获取所有文件
|
||||
var files []models.SystemFile
|
||||
_, err := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&files)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取文件列表失败: %w", err)
|
||||
}
|
||||
|
||||
m.progress.Total = len(files)
|
||||
|
||||
// 并发迁移(限制并发数)
|
||||
concurrency := 5
|
||||
sem := make(chan struct{}, concurrency)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range files {
|
||||
wg.Add(1)
|
||||
go func(file *models.SystemFile) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{} // 获取信号量
|
||||
defer func() { <-sem }() // 释放信号量
|
||||
|
||||
if err := m.MigrateFile(file); err != nil {
|
||||
m.progress.AddFailed(fmt.Sprintf("%s: %v", file.Name, err))
|
||||
}
|
||||
}(&files[i])
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProgress 获取迁移进度
|
||||
func (m *StorageMigration) GetProgress() *MigrationProgress {
|
||||
return m.progress
|
||||
}
|
||||
|
||||
// MigrateLocalToQiniu 从本地存储迁移到七牛云
|
||||
func MigrateLocalToQiniu(tid uint64) (*MigrationProgress, error) {
|
||||
// 获取存储配置
|
||||
cfg, err := models.GetStorageConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取存储配置失败: %w", err)
|
||||
}
|
||||
|
||||
if cfg.StorageType != "qiniu" {
|
||||
return nil, fmt.Errorf("当前存储类型不是七牛云")
|
||||
}
|
||||
|
||||
// 创建存储服务
|
||||
localStorage := NewLocalStorage()
|
||||
qiniuStorage := NewQiniuStorage(cfg)
|
||||
|
||||
// 创建迁移服务
|
||||
migration := NewStorageMigration(localStorage, qiniuStorage)
|
||||
|
||||
// 执行迁移
|
||||
if err := migration.MigrateAll(tid); err != nil {
|
||||
return migration.GetProgress(), err
|
||||
}
|
||||
|
||||
return migration.GetProgress(), nil
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
||||
"github.com/qiniu/go-sdk/v7/storage"
|
||||
)
|
||||
|
||||
// StorageService 存储服务接口
|
||||
type StorageService interface {
|
||||
Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error)
|
||||
GetPublicURL(key string) string
|
||||
Delete(key string) error
|
||||
}
|
||||
|
||||
// UploadResult 上传结果
|
||||
type UploadResult struct {
|
||||
URL string // 完整访问URL
|
||||
Key string // 存储key/路径
|
||||
Size int64 // 文件大小
|
||||
MD5 string // 文件MD5
|
||||
MimeType string // 文件类型
|
||||
}
|
||||
|
||||
// LocalStorage 本地存储实现
|
||||
type LocalStorage struct {
|
||||
BaseDir string // 基础目录,默认 "uploads"
|
||||
BaseURL string // 基础URL,默认 "/"
|
||||
}
|
||||
|
||||
// NewLocalStorage 创建本地存储服务
|
||||
func NewLocalStorage() *LocalStorage {
|
||||
return &LocalStorage{
|
||||
BaseDir: "uploads",
|
||||
BaseURL: "/",
|
||||
}
|
||||
}
|
||||
|
||||
// Upload 上传文件到本地
|
||||
func (s *LocalStorage) Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) {
|
||||
// 生成存储路径
|
||||
ext := filepath.Ext(header.Filename)
|
||||
datePath := time.Now().Format("2006/01/02")
|
||||
fileName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
|
||||
savePath := filepath.Join(datePath, fileName)
|
||||
|
||||
// 创建目录
|
||||
destDir := filepath.Join(s.BaseDir, filepath.FromSlash(datePath))
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
destPath := filepath.Join(s.BaseDir, filepath.FromSlash(savePath))
|
||||
dst, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// 计算MD5并复制文件
|
||||
hash := md5.New()
|
||||
size, err := io.Copy(io.MultiWriter(dst, hash), file)
|
||||
if err != nil {
|
||||
_ = os.Remove(destPath)
|
||||
return nil, fmt.Errorf("保存文件失败: %w", err)
|
||||
}
|
||||
|
||||
md5Sum := hex.EncodeToString(hash.Sum(nil))
|
||||
webURL := s.BaseURL + strings.ReplaceAll(filepath.ToSlash(destPath), "\\", "/")
|
||||
|
||||
return &UploadResult{
|
||||
URL: webURL,
|
||||
Key: savePath,
|
||||
Size: size,
|
||||
MD5: md5Sum,
|
||||
MimeType: header.Header.Get("Content-Type"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPublicURL 获取公开访问URL
|
||||
func (s *LocalStorage) GetPublicURL(key string) string {
|
||||
return s.BaseURL + filepath.ToSlash(filepath.Join(s.BaseDir, key))
|
||||
}
|
||||
|
||||
// Delete 删除本地文件
|
||||
func (s *LocalStorage) Delete(key string) error {
|
||||
filePath := filepath.Join(s.BaseDir, filepath.FromSlash(key))
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
// QiniuStorage 七牛云存储实现
|
||||
type QiniuStorage struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Bucket string
|
||||
Domain string
|
||||
Region string
|
||||
}
|
||||
|
||||
// NewQiniuStorage 创建七牛云存储服务
|
||||
func NewQiniuStorage(cfg *models.StorageConfig) *QiniuStorage {
|
||||
return &QiniuStorage{
|
||||
AccessKey: cfg.QiniuAccessKey,
|
||||
SecretKey: cfg.QiniuSecretKey,
|
||||
Bucket: cfg.QiniuBucket,
|
||||
Domain: cfg.QiniuDomain,
|
||||
Region: cfg.QiniuRegion,
|
||||
}
|
||||
}
|
||||
|
||||
// getZone 根据区域代码获取存储区域
|
||||
func (s *QiniuStorage) getZone() *storage.Region {
|
||||
switch s.Region {
|
||||
case "z0":
|
||||
return &storage.ZoneHuadong
|
||||
case "z1":
|
||||
return &storage.ZoneHuabei
|
||||
case "z2":
|
||||
return &storage.ZoneHuanan
|
||||
case "na0":
|
||||
return &storage.ZoneBeimei
|
||||
case "as0":
|
||||
return &storage.ZoneXinjiapo
|
||||
case "cn-east-2":
|
||||
return &storage.ZoneHuadongZheJiang2
|
||||
default:
|
||||
return &storage.ZoneHuadong // 默认华东
|
||||
}
|
||||
}
|
||||
|
||||
// Upload 上传文件到七牛云
|
||||
func (s *QiniuStorage) Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) {
|
||||
// 生成存储key
|
||||
ext := filepath.Ext(header.Filename)
|
||||
datePath := time.Now().Format("2006/01/02")
|
||||
fileName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
|
||||
key := filepath.ToSlash(filepath.Join(datePath, fileName))
|
||||
|
||||
// 创建上传凭证
|
||||
mac := qbox.NewMac(s.AccessKey, s.SecretKey)
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: s.Bucket,
|
||||
}
|
||||
upToken := putPolicy.UploadToken(mac)
|
||||
|
||||
// 配置上传参数
|
||||
cfg := storage.Config{
|
||||
Region: s.getZone(),
|
||||
UseHTTPS: true,
|
||||
UseCdnDomains: false,
|
||||
}
|
||||
|
||||
// 创建表单上传器
|
||||
formUploader := storage.NewFormUploader(&cfg)
|
||||
ret := storage.PutRet{}
|
||||
putExtra := storage.PutExtra{}
|
||||
|
||||
// 计算文件大小和MD5
|
||||
tmpFile, err := os.CreateTemp("", "qiniu_upload_*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建临时文件失败: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
defer tmpFile.Close()
|
||||
|
||||
hash := md5.New()
|
||||
size, err := io.Copy(io.MultiWriter(tmpFile, hash), file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取文件失败: %w", err)
|
||||
}
|
||||
md5Sum := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
// 重置文件指针
|
||||
if _, err := tmpFile.Seek(0, 0); err != nil {
|
||||
return nil, fmt.Errorf("重置文件指针失败: %w", err)
|
||||
}
|
||||
|
||||
// 执行上传
|
||||
err = formUploader.Put(context.Background(), &ret, upToken, key, tmpFile, size, &putExtra)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("上传到七牛云失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建完整URL
|
||||
domain := strings.TrimRight(s.Domain, "/")
|
||||
url := fmt.Sprintf("%s/%s", domain, ret.Key)
|
||||
|
||||
return &UploadResult{
|
||||
URL: url,
|
||||
Key: ret.Key,
|
||||
Size: size,
|
||||
MD5: md5Sum,
|
||||
MimeType: header.Header.Get("Content-Type"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPublicURL 获取七牛云公开访问URL
|
||||
func (s *QiniuStorage) GetPublicURL(key string) string {
|
||||
domain := strings.TrimRight(s.Domain, "/")
|
||||
return fmt.Sprintf("%s/%s", domain, key)
|
||||
}
|
||||
|
||||
// Delete 删除七牛云文件
|
||||
func (s *QiniuStorage) Delete(key string) error {
|
||||
mac := qbox.NewMac(s.AccessKey, s.SecretKey)
|
||||
cfg := storage.Config{
|
||||
Region: s.getZone(),
|
||||
UseHTTPS: true,
|
||||
}
|
||||
|
||||
bucketManager := storage.NewBucketManager(mac, &cfg)
|
||||
err := bucketManager.Delete(s.Bucket, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除七牛云文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStorageService 根据配置获取存储服务
|
||||
func GetStorageService() (StorageService, error) {
|
||||
cfg, err := models.GetStorageConfig()
|
||||
if err != nil {
|
||||
// 默认使用本地存储
|
||||
return NewLocalStorage(), nil
|
||||
}
|
||||
|
||||
switch cfg.StorageType {
|
||||
case "qiniu":
|
||||
if cfg.QiniuAccessKey == "" || cfg.QiniuSecretKey == "" ||
|
||||
cfg.QiniuBucket == "" || cfg.QiniuDomain == "" {
|
||||
return nil, fmt.Errorf("七牛云配置不完整")
|
||||
}
|
||||
return NewQiniuStorage(cfg), nil
|
||||
case "local":
|
||||
return NewLocalStorage(), nil
|
||||
default:
|
||||
return NewLocalStorage(), nil
|
||||
}
|
||||
}
|
||||
+77
-16
@@ -1,11 +1,15 @@
|
||||
package services
|
||||
|
||||
import "server/models"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// BindTenantUser 绑定用户到租户(若已存在则更新状态/默认值)
|
||||
func BindTenantUser(tid, uid uint64, account, name, phone, email, password *string, isDefault, status int8, remark *string) (uint64, error) {
|
||||
var existed models.TenantUser
|
||||
err := models.Orm.QueryTable(new(models.TenantUser)).
|
||||
func BindTenantUser(tid, uid uint64, account, name, phone, email *string, sex *uint8, birth *string, password *string, isDefault, status int8, remark *string) (uint64, error) {
|
||||
var existed models.SystemTenantUser
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("uid", uid).
|
||||
One(&existed)
|
||||
@@ -20,11 +24,22 @@ func BindTenantUser(tid, uid uint64, account, name, phone, email, password *stri
|
||||
"is_default": isDefault,
|
||||
"remark": remark,
|
||||
}
|
||||
_, uErr := models.Orm.QueryTable(new(models.TenantUser)).Filter("id", existed.ID).Update(update)
|
||||
if sex != nil {
|
||||
update["sex"] = *sex
|
||||
}
|
||||
if birth != nil {
|
||||
trimmedBirth := strings.TrimSpace(*birth)
|
||||
if trimmedBirth == "" {
|
||||
update["birth"] = nil
|
||||
} else {
|
||||
update["birth"] = trimmedBirth
|
||||
}
|
||||
}
|
||||
_, uErr := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", existed.ID).Update(update)
|
||||
return existed.ID, uErr
|
||||
}
|
||||
|
||||
m := &models.TenantUser{
|
||||
m := &models.SystemTenantUser{
|
||||
Tid: tid,
|
||||
Uid: uid,
|
||||
Account: account,
|
||||
@@ -36,20 +51,29 @@ func BindTenantUser(tid, uid uint64, account, name, phone, email, password *stri
|
||||
Status: status,
|
||||
Remark: remark,
|
||||
}
|
||||
if sex != nil {
|
||||
m.Sex = *sex
|
||||
}
|
||||
if birth != nil {
|
||||
trimmedBirth := strings.TrimSpace(*birth)
|
||||
if trimmedBirth != "" {
|
||||
m.Birth = &trimmedBirth
|
||||
}
|
||||
}
|
||||
id, iErr := models.Orm.Insert(m)
|
||||
return uint64(id), iErr
|
||||
}
|
||||
|
||||
// UnbindTenantUser 删除绑定关系
|
||||
func UnbindTenantUser(id uint64) error {
|
||||
_, err := models.Orm.QueryTable(new(models.TenantUser)).Filter("id", id).Delete()
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).Delete()
|
||||
return err
|
||||
}
|
||||
|
||||
// ListTenantUsersByTid 根据租户ID查询绑定关系
|
||||
func ListTenantUsersByTid(tid uint64) ([]models.TenantUser, error) {
|
||||
var rows []models.TenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.TenantUser)).
|
||||
func ListTenantUsersByTid(tid uint64) ([]models.SystemTenantUser, error) {
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
@@ -57,27 +81,64 @@ func ListTenantUsersByTid(tid uint64) ([]models.TenantUser, error) {
|
||||
}
|
||||
|
||||
// ListTenantBindingsByUid 根据用户ID查询绑定关系
|
||||
func ListTenantBindingsByUid(uid uint64) ([]models.TenantUser, error) {
|
||||
var rows []models.TenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.TenantUser)).
|
||||
func ListTenantBindingsByUid(uid uint64) ([]models.SystemTenantUser, error) {
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// GetTenantUserByUidAndTid 根据用户ID和租户ID查询租户用户绑定关系
|
||||
func GetTenantUserByUidAndTid(uid, tid uint64) (*models.SystemTenantUser, error) {
|
||||
var row models.SystemTenantUser
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
Filter("tid", tid).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// GetTenantUserByUid 根据用户ID查询默认/最新租户用户绑定关系
|
||||
func GetTenantUserByUid(uid uint64) (*models.SystemTenantUser, error) {
|
||||
var row models.SystemTenantUser
|
||||
err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
OrderBy("-is_default", "-id").
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// GetTenantByID 根据租户ID查询租户信息
|
||||
func GetTenantByID(id uint64) (*models.SystemTenant, error) {
|
||||
var row models.SystemTenant
|
||||
err := models.Orm.QueryTable(new(models.SystemTenant)).
|
||||
Filter("id", id).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// SetDefaultTenant 设置用户默认租户(同一用户仅一个默认)
|
||||
func SetDefaultTenant(uid, tid uint64) error {
|
||||
_, err := models.Orm.QueryTable(new(models.TenantUser)).Filter("uid", uid).Update(map[string]interface{}{
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("uid", uid).Update(map[string]interface{}{
|
||||
"is_default": 0,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = models.Orm.QueryTable(new(models.TenantUser)).
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", uid).
|
||||
Filter("tid", tid).
|
||||
Update(map[string]interface{}{"is_default": 1})
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user