Compare commits

...
30 Commits
Author SHA1 Message Date
hero920103 3a57f1cf14 设备管理优化 2026-06-16 00:39:20 +08:00
hero920103 6788d49f47 增加续杯激活 2026-06-15 23:43:49 +08:00
hero920103 6b8651af25 增加续杯接口 2026-06-15 23:01:01 +08:00
hero920103 df02280086 修复检测 2026-06-14 00:45:38 +08:00
hero920103 269fbd08ff 更新补号机制 2026-06-02 23:44:44 +08:00
hero920103 e6b84aad80 gengxin 2026-06-02 21:15:13 +08:00
hero920103 31fc86e878 更新接口 2026-06-01 22:35:56 +08:00
hero920103 6d9977cb76 更新backend相关问题 2026-06-01 18:08:32 +08:00
hero920103 e685c9c0c7 修复筛选cursor列表 2026-05-21 18:31:56 +08:00
hero920103 6de78a5a2a 更新erp 2026-05-13 08:37:54 +08:00
hero920103 ed34505ea9 更新 2026-05-08 09:02:59 +08:00
hero920103 283a2b7a80 增加token检测 2026-05-05 23:54:15 +08:00
hero920103 2a60d34711 更改统计 2026-05-05 18:31:29 +08:00
hero920103 824199c87c 修复补号功能 2026-05-05 17:56:42 +08:00
hero920103 61c151f62a Merge branch 'master' of https://git.yunzer.cn/hero920103/go-platform 2026-04-29 18:25:40 +08:00
hero920103 fd699b2821 补充 2026-04-29 18:25:27 +08:00
hero920103 0aed67fb95 更新后端 2026-04-27 10:50:47 +08:00
hero920103 d9df691298 更新号池功能 2026-04-15 23:20:40 +08:00
hero920103 53decd0084 更新获取接口 2026-04-13 10:09:00 +08:00
hero920103 ccad4c05f7 增加提取接口 2026-04-13 09:24:42 +08:00
hero920103 fa72952f15 批量更新 2026-04-13 08:36:57 +08:00
hero920103 ea0e84c93c 修复七牛云上传bug 2026-04-09 19:27:47 +08:00
hero920103 f4403701c1 解决登录报错问题 2026-04-09 18:50:59 +08:00
hero920103 d9f0abfc10 修补2 2026-04-09 18:23:43 +08:00
hero920103 5596f9da22 修补1 2026-04-09 18:21:51 +08:00
hero920103 3d1a1c9711 修补bug 2026-04-09 18:19:23 +08:00
hero920103 f84df652d9 改七牛直传 2026-04-09 18:07:59 +08:00
hero920103 6df84b0584 增加自动安装脚本 2026-04-09 17:51:11 +08:00
hero920103 4a4bd8f67c 解决超时问题 2026-04-09 17:08:05 +08:00
hero920103 36d2a8945e 增加七牛云存储 2026-04-09 16:26:38 +08:00
72 changed files with 47766 additions and 579 deletions
+12
View File
@@ -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
+503
View File
@@ -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,
})
}
+214
View File
@@ -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
}
}
+605
View File
@@ -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()
}
+954
View File
@@ -0,0 +1,954 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// BackendArticleController CMS 文章管理
type BackendArticleController struct {
beego.Controller
}
// BackendArticleCategoryController CMS 文章分类管理
type BackendArticleCategoryController struct {
beego.Controller
}
func (c *BackendArticleController) cmsClaims() (*jwtutil.Claims, error) {
return cmsBackendClaims(&c.Controller)
}
func (c *BackendArticleCategoryController) cmsClaims() (*jwtutil.Claims, error) {
return cmsBackendClaims(&c.Controller)
}
func cmsBackendClaims(c *beego.Controller) (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
}
if claims.UserType != "backend" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
func cmsEffectiveTid(c *beego.Controller, claims *jwtutil.Claims) uint64 {
_ = c.ParseForm(1 << 20)
if tid, err := c.GetUint64("tid"); err == nil && tid > 0 {
return tid
}
if h := strings.TrimSpace(c.Ctx.Request.Header.Get("X-Tenant-Id")); h != "" {
if v, e := strconv.ParseUint(h, 10, 64); e == nil {
return v
}
}
if claims != nil && claims.TenantId > 0 {
return uint64(claims.TenantId)
}
return 0
}
func (c *BackendArticleController) cmsJSONErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *BackendArticleCategoryController) cmsJSONErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func cmsEnsureTables(c *beego.Controller) bool {
if err := models.EnsureCmsArticleTables(); err != nil {
c.Ctx.Output.SetStatus(500)
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "初始化文章表失败: " + err.Error()}
_ = c.ServeJSON()
return false
}
return true
}
func cmsParseUintArg(v interface{}) uint64 {
switch x := v.(type) {
case float64:
if x > 0 {
return uint64(x)
}
case string:
if n, err := strconv.ParseUint(strings.TrimSpace(x), 10, 64); err == nil {
return n
}
}
return 0
}
func cmsArticleToListItem(row models.CmsArticle, cateName string) map[string]interface{} {
return map[string]interface{}{
"id": row.ID,
"title": row.Title,
"author": row.Author,
"cate": cateName,
"cate_id": row.CateID,
"status": row.Status,
"views": row.Views,
"likes": row.Likes,
"top": row.Top,
"recommend": row.Recommend,
"publish_date": models.CmsFormatTime(row.PublishTime),
"update_time": models.CmsFormatTime(row.UpdateTime),
}
}
func cmsArticleToDetail(row models.CmsArticle, cateName string) map[string]interface{} {
pub := models.CmsFormatTime(row.PublishTime)
return map[string]interface{}{
"id": row.ID,
"title": row.Title,
"author": row.Author,
"cate": cateName,
"cate_id": row.CateID,
"content": row.Content,
"desc": row.Desc,
"image": row.Image,
"is_trans": row.IsTrans,
"transurl": row.TransURL,
"status": row.Status,
"views": row.Views,
"view_count": row.Views,
"likes": row.Likes,
"top": row.Top,
"recommend": row.Recommend,
"publish_time": pub,
"publish_date": pub,
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
"update_time": models.CmsFormatTime(row.UpdateTime),
}
}
func cmsCategoryToMap(row models.CmsArticleCategory) map[string]interface{} {
return map[string]interface{}{
"id": row.ID,
"name": row.Name,
"label": row.Name,
"cid": row.Cid,
"parentId": row.Cid,
"image": row.Image,
"desc": row.Desc,
"remark": row.Desc,
"sort": row.Sort,
"status": row.Status,
}
}
// List GET /backend/articlesList
func (c *BackendArticleController) List() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
if tid == 0 {
c.cmsJSONErr(400, 400, "tid不能为空")
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 10)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 200 {
pageSize = 200
}
keyword := strings.TrimSpace(c.GetString("keyword"))
cateFilter := strings.TrimSpace(c.GetString("cate"))
qs := models.Orm.QueryTable(new(models.CmsArticle)).
Filter("tid", tid).
Filter("delete_time__isnull", true)
if keyword != "" {
qs = qs.Filter("title__icontains", keyword)
}
if cateFilter != "" {
if cid, err := strconv.ParseUint(cateFilter, 10, 64); err == nil && cid > 0 {
qs = qs.Filter("cate_id", cid)
}
}
total, _ := qs.Count()
var rows []models.CmsArticle
offset := (page - 1) * pageSize
_, err = qs.OrderBy("-top", "-id").Limit(pageSize, offset).All(&rows)
if err != nil && err != orm.ErrNoRows {
c.cmsJSONErr(500, 500, "获取文章列表失败")
return
}
cateIDs := make([]uint64, 0, len(rows))
for _, r := range rows {
if r.CateID > 0 {
cateIDs = append(cateIDs, r.CateID)
}
}
cateNames := models.CmsCategoryNameMap(tid, cateIDs)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, cmsArticleToListItem(r, cateNames[r.CateID]))
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"list": list, "total": total},
}
_ = c.ServeJSON()
}
// ListAll GET /backend/allarticles
func (c *BackendArticleController) ListAll() {
c.List()
}
// Detail GET /backend/articles/:id
func (c *BackendArticleController) Detail() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
var row models.CmsArticle
err = models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
One(&row)
if err == orm.ErrNoRows {
c.cmsJSONErr(404, 404, "文章不存在")
return
}
if err != nil {
c.cmsJSONErr(500, 500, "查询失败")
return
}
cateName := ""
if row.CateID > 0 {
names := models.CmsCategoryNameMap(tid, []uint64{row.CateID})
cateName = names[row.CateID]
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": cmsArticleToDetail(row, cateName),
}
_ = c.ServeJSON()
}
type cmsArticlePayload struct {
Title string `json:"title"`
Author string `json:"author"`
Cate interface{} `json:"cate"`
Content string `json:"content"`
Desc string `json:"desc"`
Image string `json:"image"`
IsTrans int8 `json:"is_trans"`
TransURL *string `json:"transurl"`
Status int8 `json:"status"`
IgnoreSimilarity int `json:"ignore_similarity"`
}
// Create POST /backend/createarticle
func (c *BackendArticleController) Create() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
if tid == 0 {
c.cmsJSONErr(400, 400, "tid不能为空")
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
var p cmsArticlePayload
if err := json.Unmarshal(raw, &p); err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
title := strings.TrimSpace(p.Title)
if title == "" {
c.cmsJSONErr(400, 400, "标题不能为空")
return
}
if p.IgnoreSimilarity != 1 {
similar, serr := models.CmsSimilarArticles(tid, title, 5)
if serr == nil && len(similar) > 0 {
c.Ctx.Output.SetStatus(409)
c.Data["json"] = map[string]interface{}{
"code": 409,
"msg": "检测到相似标题",
"data": map[string]interface{}{"similar_articles": similar},
}
_ = c.ServeJSON()
return
}
}
now := time.Now()
cateID := cmsParseUintArg(p.Cate)
row := models.CmsArticle{
Tid: tid,
Title: title,
Author: strings.TrimSpace(p.Author),
CateID: cateID,
Content: p.Content,
Desc: strings.TrimSpace(p.Desc),
Image: strings.TrimSpace(p.Image),
IsTrans: p.IsTrans,
TransURL: p.TransURL,
Status: p.Status,
CreateTime: now,
UpdateTime: &now,
}
id, err := models.Orm.Insert(&row)
if err != nil {
c.cmsJSONErr(500, 500, "创建失败")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功", "data": map[string]interface{}{"id": id}}
_ = c.ServeJSON()
}
// Update POST /backend/editarticle/:id
func (c *BackendArticleController) Update() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
var p cmsArticlePayload
if err := json.Unmarshal(raw, &p); err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
now := time.Now()
fields := map[string]interface{}{
"title": strings.TrimSpace(p.Title),
"author": strings.TrimSpace(p.Author),
"cate_id": cmsParseUintArg(p.Cate),
"content": p.Content,
"desc": strings.TrimSpace(p.Desc),
"image": strings.TrimSpace(p.Image),
"is_trans": p.IsTrans,
"transurl": p.TransURL,
"status": p.Status,
"update_time": now,
}
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(fields)
if err != nil {
c.cmsJSONErr(500, 500, "更新失败")
return
}
if n == 0 {
c.cmsJSONErr(404, 404, "文章不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
_ = c.ServeJSON()
}
// Delete DELETE /backend/deletearticle/:id
func (c *BackendArticleController) Delete() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
c.cmsJSONErr(500, 500, "删除失败")
return
}
if n == 0 {
c.cmsJSONErr(404, 404, "文章不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
_ = c.ServeJSON()
}
func (c *BackendArticleController) setArticleFlag(field string, value int8) {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
now := time.Now()
fields := map[string]interface{}{field: value, "update_time": now}
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(fields)
if err != nil {
c.cmsJSONErr(500, 500, "操作失败")
return
}
if n == 0 {
c.cmsJSONErr(404, 404, "文章不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
func (c *BackendArticleController) Publish() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
var uid uint64
raw, _ := io.ReadAll(c.Ctx.Request.Body)
if len(raw) > 0 {
var body struct {
UID uint64 `json:"uid"`
}
_ = json.Unmarshal(raw, &body)
uid = body.UID
}
if uid == 0 && claims != nil {
uid = uint64(claims.UserID)
}
now := time.Now()
fields := map[string]interface{}{
"status": int8(2),
"publish_time": now,
"publisher_id": uid,
"update_time": now,
}
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(fields)
if err != nil {
c.cmsJSONErr(500, 500, "发布失败")
return
}
if n == 0 {
c.cmsJSONErr(404, 404, "文章不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "发布成功"}
_ = c.ServeJSON()
}
func (c *BackendArticleController) Unpublish() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.CmsArticle)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"status": int8(3), "update_time": now})
if err != nil {
c.cmsJSONErr(500, 500, "下架失败")
return
}
if n == 0 {
c.cmsJSONErr(404, 404, "文章不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "下架成功"}
_ = c.ServeJSON()
}
func (c *BackendArticleController) Recommend() { c.setArticleFlag("recommend", 1) }
func (c *BackendArticleController) Unrecommend() { c.setArticleFlag("recommend", 0) }
func (c *BackendArticleController) Top() { c.setArticleFlag("top", 1) }
func (c *BackendArticleController) Untop() { c.setArticleFlag("top", 0) }
// List GET /backend/categories
func (c *BackendArticleCategoryController) List() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
if tid == 0 {
c.cmsJSONErr(400, 400, "tid不能为空")
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 0)
if pageSize == 0 {
pageSize, _ = c.GetInt("limit", 1000)
}
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 1000
}
keyword := strings.TrimSpace(c.GetString("keyword"))
qs := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("tid", tid).
Filter("delete_time__isnull", true)
if keyword != "" {
qs = qs.Filter("name__icontains", keyword)
}
total, _ := qs.Count()
var rows []models.CmsArticleCategory
offset := (page - 1) * pageSize
_, err = qs.OrderBy("sort", "id").Limit(pageSize, offset).All(&rows)
if err != nil && err != orm.ErrNoRows {
c.cmsJSONErr(500, 500, "获取分类失败")
return
}
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, cmsCategoryToMap(r))
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"list": list, "total": total, "records": list},
}
_ = c.ServeJSON()
}
// ListAll GET /backend/allcategories
func (c *BackendArticleCategoryController) ListAll() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
if tid == 0 {
c.cmsJSONErr(400, 400, "tid不能为空")
return
}
keyword := strings.TrimSpace(c.GetString("keyword"))
qs := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("tid", tid).
Filter("delete_time__isnull", true)
if keyword != "" {
qs = qs.Filter("name__icontains", keyword)
}
var rows []models.CmsArticleCategory
_, err = qs.OrderBy("sort", "id").All(&rows)
if err != nil && err != orm.ErrNoRows {
c.cmsJSONErr(500, 500, "获取分类失败")
return
}
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, cmsCategoryToMap(r))
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": list}
_ = c.ServeJSON()
}
// Detail GET /backend/categories/:id
func (c *BackendArticleCategoryController) Detail() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
var row models.CmsArticleCategory
err = models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
One(&row)
if err == orm.ErrNoRows {
c.cmsJSONErr(404, 404, "分类不存在")
return
}
if err != nil {
c.cmsJSONErr(500, 500, "查询失败")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": cmsCategoryToMap(row)}
_ = c.ServeJSON()
}
type cmsCategoryPayload struct {
Name string `json:"name"`
Image string `json:"image"`
Desc string `json:"desc"`
Sort int `json:"sort"`
Status int8 `json:"status"`
Cid uint64 `json:"cid"`
}
// Create POST /backend/createCategory
func (c *BackendArticleCategoryController) Create() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
if tid == 0 {
c.cmsJSONErr(400, 400, "tid不能为空")
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
var p cmsCategoryPayload
if err := json.Unmarshal(raw, &p); err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
name := strings.TrimSpace(p.Name)
if name == "" {
c.cmsJSONErr(400, 400, "分类名称不能为空")
return
}
now := time.Now()
row := models.CmsArticleCategory{
Tid: tid,
Cid: p.Cid,
Name: name,
Image: strings.TrimSpace(p.Image),
Desc: strings.TrimSpace(p.Desc),
Sort: p.Sort,
Status: p.Status,
CreateTime: now,
UpdateTime: &now,
}
id, err := models.Orm.Insert(&row)
if err != nil {
c.cmsJSONErr(500, 500, "创建失败")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功", "data": map[string]interface{}{"id": id}}
_ = c.ServeJSON()
}
// Update POST /backend/editCategory/:id
func (c *BackendArticleCategoryController) Update() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
var p cmsCategoryPayload
if err := json.Unmarshal(raw, &p); err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{
"name": strings.TrimSpace(p.Name),
"image": strings.TrimSpace(p.Image),
"desc": strings.TrimSpace(p.Desc),
"sort": p.Sort,
"status": p.Status,
"cid": p.Cid,
"update_time": now,
})
if err != nil {
c.cmsJSONErr(500, 500, "更新失败")
return
}
if n == 0 {
c.cmsJSONErr(404, 404, "分类不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
_ = c.ServeJSON()
}
// Delete DELETE /backend/categories/:id
func (c *BackendArticleCategoryController) Delete() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
childCnt, _ := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("tid", tid).
Filter("cid", id).
Filter("delete_time__isnull", true).
Count()
if childCnt > 0 {
c.cmsJSONErr(400, 400, "请先删除子分类")
return
}
articleCnt, _ := models.Orm.QueryTable(new(models.CmsArticle)).
Filter("tid", tid).
Filter("cate_id", id).
Filter("delete_time__isnull", true).
Count()
if articleCnt > 0 {
c.cmsJSONErr(400, 400, "该分类下还有文章,无法删除")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
c.cmsJSONErr(500, 500, "删除失败")
return
}
if n == 0 {
c.cmsJSONErr(404, 404, "分类不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
_ = c.ServeJSON()
}
// UpdateStatus PATCH /backend/categories/:id/status
func (c *BackendArticleCategoryController) UpdateStatus() {
if !cmsEnsureTables(&c.Controller) {
return
}
claims, err := c.cmsClaims()
if err != nil {
c.cmsJSONErr(401, 401, err.Error())
return
}
tid := cmsEffectiveTid(&c.Controller, claims)
id, _ := c.GetUint64(":id")
if id == 0 {
c.cmsJSONErr(400, 400, "无效ID")
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
var p struct {
Status int8 `json:"status"`
}
if err := json.Unmarshal(raw, &p); err != nil {
c.cmsJSONErr(400, 400, "参数错误")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.CmsArticleCategory)).
Filter("id", id).
Filter("tid", tid).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"status": p.Status, "update_time": now})
if err != nil {
c.cmsJSONErr(500, 500, "更新失败")
return
}
if n == 0 {
c.cmsJSONErr(404, 404, "分类不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
_ = c.ServeJSON()
}
+322
View File
@@ -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": "发送找回密码验证码暂未实现",
})
}
+600
View File
@@ -0,0 +1,600 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// BackendDomainPoolController 主域名池管理
type BackendDomainPoolController struct {
beego.Controller
}
// BackendTenantDomainController 租户域名管理
type BackendTenantDomainController struct {
beego.Controller
}
func requireBackend(c *beego.Controller) (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
}
if claims.UserType != "backend" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
// ===== 主域名池 =====
// Index GET /backend/domain/pool/index?page=&pageSize=&main_domain=&status=
func (c *BackendDomainPoolController) Index() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 10)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 200 {
pageSize = 200
}
mainDomain := strings.TrimSpace(c.GetString("main_domain"))
statusStr := strings.TrimSpace(c.GetString("status"))
qs := models.Orm.QueryTable(new(models.SystemDomainPool)).Filter("delete_time__isnull", true)
if mainDomain != "" {
qs = qs.Filter("main_domain__icontains", mainDomain)
}
if statusStr != "" {
if st, err := strconv.Atoi(statusStr); err == nil {
qs = qs.Filter("status", st)
}
}
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取主域名池失败: "+err.Error())
return
}
var rows []models.SystemDomainPool
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取主域名池失败: "+err.Error())
return
}
list := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
item := map[string]interface{}{
"id": rows[i].ID,
"main_domain": rows[i].MainDomain,
"status": rows[i].Status,
"create_time": rows[i].CreateTime.Format("2006-01-02 15:04:05"),
"update_time": "",
}
if rows[i].UpdateTime != nil {
item["update_time"] = rows[i].UpdateTime.Format("2006-01-02 15:04:05")
}
list = append(list, item)
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"list": list,
"total": total,
},
}
_ = c.ServeJSON()
}
// GetEnabledDomains GET /backend/domain/pool/getEnabledDomains
func (c *BackendDomainPoolController) GetEnabledDomains() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
var rows []models.SystemDomainPool
_, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("status", 1).
Filter("delete_time__isnull", true).
OrderBy("-id").
All(&rows)
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取主域名失败: "+err.Error())
return
}
out := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
out = append(out, map[string]interface{}{
"id": rows[i].ID,
"main_domain": rows[i].MainDomain,
"status": rows[i].Status,
})
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": out}
_ = c.ServeJSON()
}
// Create POST /backend/domain/pool/create
func (c *BackendDomainPoolController) Create() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p domainPoolPayload
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
md := strings.TrimSpace(p.MainDomain)
if md == "" {
jsonErr(&c.Controller, 400, 400, "主域名不能为空")
return
}
if p.Status != 0 && p.Status != 1 {
p.Status = 1
}
// 简单去重
cnt, _ := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("main_domain", md).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
jsonErr(&c.Controller, 400, 400, "主域名已存在")
return
}
row := &models.SystemDomainPool{MainDomain: md, Status: p.Status}
if _, err := models.Orm.Insert(row); err != nil {
jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "创建成功"}
_ = c.ServeJSON()
}
// Update POST /backend/domain/pool/update
func (c *BackendDomainPoolController) Update() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p domainPoolPayload
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
if p.ID == 0 {
jsonErr(&c.Controller, 400, 400, "id 不能为空")
return
}
md := strings.TrimSpace(p.MainDomain)
if md == "" {
jsonErr(&c.Controller, 400, 400, "主域名不能为空")
return
}
if p.Status != 0 && p.Status != 1 {
p.Status = 1
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("id", p.ID).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"main_domain": md, "status": p.Status, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "记录不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功"}
_ = c.ServeJSON()
}
// Delete DELETE /backend/domain/pool/delete/:id
func (c *BackendDomainPoolController) Delete() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
jsonErr(&c.Controller, 400, 400, "无效ID")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "记录不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
_ = c.ServeJSON()
}
// ToggleStatus POST /backend/domain/pool/toggleStatus body:{id}
func (c *BackendDomainPoolController) ToggleStatus() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p struct {
ID uint64 `json:"id"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var row models.SystemDomainPool
if err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("id", p.ID).
Filter("delete_time__isnull", true).
One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "记录不存在")
return
}
newStatus := int8(1)
if row.Status == 1 {
newStatus = 0
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("id", p.ID).
Update(map[string]interface{}{"status": newStatus, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "切换失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// ===== 租户域名 =====
// Index GET /backend/domain/tenant/index?page=&pageSize=&tid=&status=&sub_domain=
func (c *BackendTenantDomainController) Index() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 10)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 200 {
pageSize = 200
}
tid, _ := c.GetUint64("tid")
statusStr := strings.TrimSpace(c.GetString("status"))
subDomain := strings.TrimSpace(c.GetString("sub_domain"))
qs := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("delete_time__isnull", true)
if tid > 0 {
qs = qs.Filter("tid", tid)
}
if statusStr != "" {
if st, err := strconv.Atoi(statusStr); err == nil {
qs = qs.Filter("status", st)
}
}
if subDomain != "" {
qs = qs.Filter("sub_domain__icontains", subDomain)
}
total, err := qs.Count()
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取租户域名失败: "+err.Error())
return
}
var rows []models.SystemTenantDomain
_, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取租户域名失败: "+err.Error())
return
}
list := make([]models.SystemTenantDomain, 0, len(rows))
list = append(list, rows...)
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"list": list, "total": total},
}
_ = c.ServeJSON()
}
// MyDomains GET /backend/domain/tenant/myDomains?tid=1
func (c *BackendTenantDomainController) MyDomains() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
tid, _ := c.GetUint64("tid")
if tid == 0 {
jsonErr(&c.Controller, 400, 400, "租户ID不能为空")
return
}
var rows []models.SystemTenantDomain
_, err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("tid", tid).
Filter("delete_time__isnull", true).
OrderBy("-id").
All(&rows)
if err != nil {
jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": rows}
_ = c.ServeJSON()
}
// Apply POST /backend/domain/tenant/apply body:{tid,sub_domain,main_domain}
func (c *BackendTenantDomainController) Apply() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p struct {
Tid uint64 `json:"tid"`
SubDomain string `json:"sub_domain"`
MainDomain string `json:"main_domain"`
}
if err := json.Unmarshal(raw, &p); err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
if p.Tid == 0 {
jsonErr(&c.Controller, 400, 400, "租户ID不能为空")
return
}
sub := strings.TrimSpace(p.SubDomain)
main := strings.TrimSpace(p.MainDomain)
if sub == "" {
jsonErr(&c.Controller, 400, 400, "二级域名前缀不能为空")
return
}
if main == "" {
jsonErr(&c.Controller, 400, 400, "请选择主域名")
return
}
if !subDomainRe.MatchString(sub) {
jsonErr(&c.Controller, 400, 400, "二级域名前缀格式不正确")
return
}
// 该租户是否已有域名
cnt, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("tid", p.Tid).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
jsonErr(&c.Controller, 400, 400, "该租户已有域名,请删除后再次申请")
return
}
// 主域名存在且启用
var pool models.SystemDomainPool
if err := models.Orm.QueryTable(new(models.SystemDomainPool)).
Filter("main_domain", main).
Filter("status", 1).
Filter("delete_time__isnull", true).
One(&pool); err != nil {
jsonErr(&c.Controller, 400, 400, "主域名不存在或已禁用")
return
}
// 二级域名是否已被使用(同主域名下)
used, _ := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("sub_domain", sub).
Filter("main_domain", main).
Filter("delete_time__isnull", true).
Count()
if used > 0 {
jsonErr(&c.Controller, 400, 400, "该二级域名已被使用")
return
}
full := sub + "." + main
now := time.Now()
tid := p.Tid
row := &models.SystemTenantDomain{
Tid: &tid,
SubDomain: &sub,
MainDomain: &main,
FullDomain: &full,
Status: 0,
CreateTime: now,
UpdateTime: &now,
}
id, err := models.Orm.Insert(row)
if err != nil {
jsonErr(&c.Controller, 500, 500, "申请失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "申请提交成功,等待审核", "data": map[string]interface{}{"id": uint64(id)}}
_ = c.ServeJSON()
}
// Audit POST /backend/domain/tenant/audit body:{id,action} action=approve/reject
func (c *BackendTenantDomainController) Audit() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p struct {
ID uint64 `json:"id"`
Action string `json:"action"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var row models.SystemTenantDomain
if err := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "域名不存在")
return
}
if row.Status != 0 {
jsonErr(&c.Controller, 400, 400, "该域名已审核过了")
return
}
newStatus := 2
msg := "已拒绝"
if strings.ToLower(strings.TrimSpace(p.Action)) == "approve" {
newStatus = 1
msg = "审核通过"
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).Update(map[string]interface{}{
"status": newStatus,
"update_time": now,
})
if err != nil {
jsonErr(&c.Controller, 500, 500, "审核失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": msg}
_ = c.ServeJSON()
}
// ToggleStatus POST /backend/domain/tenant/toggleStatus body:{id}
func (c *BackendTenantDomainController) ToggleStatus() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var p struct {
ID uint64 `json:"id"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
var row models.SystemTenantDomain
if err := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).One(&row); err != nil {
jsonErr(&c.Controller, 404, 404, "域名不存在")
return
}
if row.Status == 0 {
jsonErr(&c.Controller, 400, 400, "审核中不可操作")
return
}
newStatus := 2
if row.Status == 2 {
newStatus = 1
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).Update(map[string]interface{}{
"status": newStatus,
"update_time": now,
})
if err != nil {
jsonErr(&c.Controller, 500, 500, "操作失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// Delete DELETE /backend/domain/tenant/delete/:id
func (c *BackendTenantDomainController) Delete() {
if _, err := requireBackend(&c.Controller); err != nil {
jsonErr(&c.Controller, 401, 401, err.Error())
return
}
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
jsonErr(&c.Controller, 400, 400, "参数错误")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemTenantDomain)).
Filter("id", id).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"delete_time": now, "update_time": now})
if err != nil {
jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
return
}
if n == 0 {
jsonErr(&c.Controller, 404, 404, "域名不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
_ = c.ServeJSON()
}
// 用于复杂筛选时可扩展:当前保留 orm.Condition import,避免被 gofmt 删除
var _ = orm.NewCondition
File diff suppressed because it is too large Load Diff
+907
View File
@@ -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()
}
+249
View File
@@ -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
View File
@@ -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 }
+70
View File
@@ -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()
}
+332
View File
@@ -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()
}
+607
View File
@@ -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()
}
+21
View File
@@ -0,0 +1,21 @@
package controllers
import (
"regexp"
beego "github.com/beego/beego/v2/server/web"
)
func jsonErr(c *beego.Controller, httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
type domainPoolPayload struct {
ID uint64 `json:"id"`
MainDomain string `json:"main_domain"`
Status int8 `json:"status"`
}
var subDomainRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$`)
File diff suppressed because it is too large Load Diff
+55 -7
View File
@@ -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")
}
+681
View File
@@ -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,
})
}
-15
View File
@@ -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 {
+54 -78
View File
@@ -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
+255
View File
@@ -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()
}
+58 -48
View File
@@ -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()
}
+14 -14
View File
@@ -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_useruid 为空时由 generateTenantUID 生成)
// CreateTenantUser 创建租户用户绑定(写入 yz_system_tenant_useruid 为空时自动生成)
// 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()
+1 -1
View File
@@ -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,
+63
View File
@@ -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 探测 Tokencursor 模块会回写 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
}
+327
View File
@@ -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 // 其他
}
+140
View File
@@ -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
}
+62
View File
@@ -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()
}
+70
View File
@@ -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
- ✅ 完成存储配置功能
- ✅ 支持本地存储和七牛云存储
- ✅ 实现文件迁移功能
- ✅ 完善文档体系
+18
View File
@@ -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
View File
@@ -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)
-26
View File
@@ -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
View File
@@ -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
View File
@@ -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
+33976
View File
File diff suppressed because it is too large Load Diff
+5 -7
View File
@@ -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
+24 -6
View File
@@ -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=
+5 -16
View File
@@ -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")
+159
View File
@@ -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
}
+73
View File
@@ -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
View File
@@ -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
+64
View File
@@ -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"
}
+28
View File
@@ -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"
}
+26
View File
@@ -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"
}
+35
View File
@@ -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
}
+23
View File
@@ -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"
}
+1 -1
View File
@@ -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"
}
-25
View File
@@ -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"
}
+26
View File
@@ -0,0 +1,26 @@
2026/04/09 17:41:49.470 [I] [server.go:281] http server Running on http://:8081
2026/04/09 17:43:09.442 [I] [server.go:281] http server Running on http://:8081
2026/04/09 17:43:09.442 [C] [server.go:298] ListenAndServe: listen tcp :8081: bind: address already in use
2026/04/09 17:43:15.715 [D] [router.go:1305] | 127.0.0.1| 200 | 265.592518ms| match| GET  /platform/usercate r:/platform/usercate
2026/04/09 17:43:15.925 [D] [router.go:1305] | 127.0.0.1| 200 | 468.017304ms| match| GET  /platform/currentUser r:/platform/currentUser
2026/04/09 17:43:16.057 [D] [router.go:1305] | 127.0.0.1| 200 | 274.492712ms| match| GET  /platform/catefiles/0 r:/platform/catefiles/:id
2026/04/09 17:43:22.620 [D] [router.go:1305] | 127.0.0.1| 200 | 277.387093ms| match| GET  /platform/usercate r:/platform/usercate
2026/04/09 17:43:22.622 [D] [router.go:1305] | 127.0.0.1| 200 | 271.734643ms| match| GET  /platform/currentUser r:/platform/currentUser
2026/04/09 17:43:23.037 [D] [router.go:1305] | 127.0.0.1| 200 | 353.76378ms| match| GET  /platform/catefiles/0 r:/platform/catefiles/:id
2026/04/09 17:43:24.492 [D] [router.go:1305] | 127.0.0.1| 200 | 351.839484ms| match| GET  /platform/catefiles/5 r:/platform/catefiles/:id
2026/04/09 17:43:25.518 [D] [router.go:1305] | 127.0.0.1| 200 | 325.277959ms| match| GET  /platform/catefiles/0 r:/platform/catefiles/:id
2026/04/09 17:43:29.495 [D] [router.go:1305] | 127.0.0.1| 200 | 10.772µs| nomatch| OPTIONS  /platform/logout
2026/04/09 17:43:29.537 [D] [router.go:1305] | 127.0.0.1| 200 | 42.423µs| match| POST  /platform/logout r:/platform/logout
2026/04/09 17:43:29.744 [D] [router.go:1305] | 127.0.0.1| 200 | 88.149485ms| match| GET  /platform/loginVerifyInfos r:/platform/loginVerifyInfos
2026/04/09 17:43:30.734 [D] [router.go:1305] | 127.0.0.1| 200 | 87.869914ms| match| GET  /platform/login/getOpenVerify r:/platform/login/getOpenVerify
2026/04/09 17:43:30.868 [D] [router.go:1305] | 127.0.0.1| 200 | 87.927568ms| match| GET  /platform/login/getGeetest4Infos r:/platform/login/getGeetest4Infos
2026/04/09 17:43:36.373 [D] [router.go:1305] | 127.0.0.1| 200 | 61.401µs| match| POST  /platform/login r:/platform/login
2026/04/09 17:43:50.709 [D] [router.go:1305] | 127.0.0.1| 200 | 89.443767ms| match| GET  /platform/loginVerifyInfos r:/platform/loginVerifyInfos
2026/04/09 17:43:52.056 [D] [router.go:1305] | 127.0.0.1| 200 | 70.997714ms| match| GET  /platform/login/getOpenVerify r:/platform/login/getOpenVerify
2026/04/09 17:43:52.175 [D] [router.go:1305] | 127.0.0.1| 200 | 74.287857ms| match| GET  /platform/login/getGeetest4Infos r:/platform/login/getGeetest4Infos
2026/04/09 17:43:57.811 [D] [router.go:1305] | 127.0.0.1| 200 | 11.75µs| nomatch| OPTIONS  /platform/login
2026/04/09 17:43:57.854 [D] [router.go:1305] | 127.0.0.1| 200 | 42.819µs| match| POST  /platform/login r:/platform/login
2026/04/09 17:43:59.739 [D] [router.go:1305] | 127.0.0.1| 200 | 70.336904ms| match| GET  /platform/login/getOpenVerify r:/platform/login/getOpenVerify
2026/04/09 17:43:59.862 [D] [router.go:1305] | 127.0.0.1| 200 | 70.920607ms| match| GET  /platform/login/getGeetest4Infos r:/platform/login/getGeetest4Infos
2026/04/09 17:44:05.289 [D] [router.go:1305] | 127.0.0.1| 200 | 11.177µs| nomatch| OPTIONS  /platform/login
2026/04/09 17:44:05.332 [D] [router.go:1305] | 127.0.0.1| 200 | 42.979µs| match| POST  /platform/login r:/platform/login
+560
View File
@@ -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 limitUTF-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+protoHTTP/2 二进制流(gRPC 兼容形态,非 JSON REST"
// cursorStreamNote 说明 rawPreview / ok 的含义边界(与「仅通 200」结论一致)
const cursorStreamNote = `【协议】本 URL 为 Cursor 官方 Agent 流式接口,请求体为 protobufrequestBodyPrefixHex 可见非表单/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)
}
+217
View File
@@ -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 中解析 profileArnKiro 暂无法自动探测",
}
}
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: "KiroAWS 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
View File
@@ -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
View File
@@ -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")
}
+78 -2
View File
@@ -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")
}
+29
View File
@@ -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
+168
View File
@@ -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 ""
+36
View File
@@ -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
+33
View File
@@ -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"
+19
View File
@@ -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
+285
View File
@@ -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 "$@"
+93
View File
@@ -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
View File
Binary file not shown.
+8 -12
View File
@@ -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
}
+8 -9
View File
@@ -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 {
+191
View File
@@ -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
}
+252
View File
@@ -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
View File
@@ -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
}