first commit

This commit is contained in:
2026-06-03 10:09:03 +08:00
commit 81f5039458
6266 changed files with 188236 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
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) {
for {
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 {
if err == orm.ErrNoRows {
c.cardErr(404, 404, "暂无可用卡密")
} else {
c.cardErr(500, 500, "查询失败")
}
return
}
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolCursor)).
Filter("id", row.ID).
Update(map[string]interface{}{
"is_extracted": 1,
"extracted_time": now,
"extracted_platform": platform,
})
if err != nil {
c.cardErr(500, 500, "提取失败")
return
}
// Cursor 号池需要先判断可用状态:is_used=1 才发送给前端;
// is_used=0(已用完/不可用)或 NULL(未探测)则继续提取下一条。
if row.IsUsed != nil && *row.IsUsed == 1 {
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType))
return
}
}
}
func (c *ApiGetCardController) extractWindsurf(platform, dataType string, now time.Time) {
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 {
if err == orm.ErrNoRows {
c.cardErr(404, 404, "暂无可用卡密")
} else {
c.cardErr(500, 500, "查询失败")
}
return
}
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolWindsurf)).
Filter("id", row.ID).
Update(map[string]interface{}{
"is_extracted": 1,
"extracted_time": now,
"extracted_platform": platform,
})
if err != nil {
c.cardErr(500, 500, "提取失败")
return
}
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType))
}
func (c *ApiGetCardController) extractKrio(platform, dataType string, now time.Time) {
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 {
if err == orm.ErrNoRows {
c.cardErr(404, 404, "暂无可用卡密")
} else {
c.cardErr(500, 500, "查询失败")
}
return
}
_, err := models.Orm.QueryTable(new(models.PlatformAccountPoolKiro)).
Filter("id", row.ID).
Update(map[string]interface{}{
"is_extracted": 1,
"extracted_time": now,
"extracted_platform": platform,
})
if err != nil {
c.cardErr(500, 500, "提取失败")
return
}
c.cardOK(buildCardResult(&row.Account, &row.Password, row.Token, row.DataType))
}
// 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
}
}
+58
View File
@@ -0,0 +1,58 @@
package controllers
import (
"strings"
"server/models"
"server/services"
beego "github.com/beego/beego/v2/server/web"
)
// ApiSoftwareUpgradeController 开放接口:客户端检查更新(无需登录)
type ApiSoftwareUpgradeController struct {
beego.Controller
}
// Check GET /api/softwareupgrade/check?code=desktop-app(可选 version 由客户端自行比对 latestVersion
func (c *ApiSoftwareUpgradeController) Check() {
code := strings.TrimSpace(c.GetString("code"))
if code == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "缺少参数 code(产品标识)"}
_ = c.ServeJSON()
return
}
var row models.SystemSoftwareUpgrade
err := models.Orm.QueryTable(new(models.SystemSoftwareUpgrade)).
Filter("code", code).
Filter("status", 1).
Filter("delete_time__isnull", true).
One(&row)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "产品不存在或已停用"}
_ = c.ServeJSON()
return
}
latest := strings.TrimSpace(row.LatestVersion)
if latest == "" {
latest = "0.0.0"
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
dl := services.ResolveSoftwareDownloadURL(scheme, host, row.DownloadURL, row.FileID)
data := map[string]interface{}{
"latestVersion": latest,
"downloadUrl": dl,
"forceUpdate": row.ForceUpdate == 1,
"releaseNotes": "",
}
if row.ReleaseNotes != nil {
data["releaseNotes"] = *row.ReleaseNotes
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
+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()
}
+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": "发送找回密码验证码暂未实现",
})
}
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()
}
+360
View File
@@ -0,0 +1,360 @@
package controllers
import (
"encoding/json"
"server/models"
"strconv"
"strings"
beego "github.com/beego/beego/v2/server/web"
)
type BackendMenuController struct {
beego.Controller
}
type AdminMenuController = BackendMenuController
type menuPayload struct {
Pid *int64 `json:"pid"`
Title *string `json:"title"`
Path *string `json:"path"`
ComponentPath *string `json:"component_path"`
Icon *string `json:"icon"`
Sort *int64 `json:"sort"`
Status *int8 `json:"status"`
IsVisible *int8 `json:"is_visible"`
Views []int `json:"views"`
Type *int8 `json:"type"`
Permission *string `json:"permission"`
}
func parseViews(raw *string) []int {
if raw == nil || strings.TrimSpace(*raw) == "" {
return nil
}
var arr []int
if err := json.Unmarshal([]byte(*raw), &arr); err != nil {
return nil
}
return arr
}
func hasView(arr []int, v int) bool {
for _, n := range arr {
if n == v {
return true
}
}
return false
}
func filterMenusByView(menus []models.SystemMenu, v int) []models.SystemMenu {
out := make([]models.SystemMenu, 0, len(menus))
for _, m := range menus {
views := parseViews(m.Views)
if v == 1 && len(views) == 0 {
out = append(out, m)
continue
}
if hasView(views, v) {
out = append(out, m)
}
}
return out
}
func (c *BackendMenuController) GetMenu() {
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, 1), 0)}
_ = c.ServeJSON()
}
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": "获取成功",
"data": map[string]interface{}{"list": tree, "total": len(tree)},
}
_ = c.ServeJSON()
}
func (c *BackendMenuController) GetAllMenus() {
var menus []models.SystemMenu
cid, _ := c.GetInt("cid")
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
}
if cid == 1 {
menus = filterMenusByView(menus, 1)
} else if cid == 2 {
menus = filterMenusByView(menus, 2)
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(menus, 0)}
_ = c.ServeJSON()
}
func (c *BackendMenuController) GetAllBackendMenus() {
var menus []models.SystemMenu
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
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(filterMenusByView(menus, 2), 0)}
_ = c.ServeJSON()
}
type menuNode struct {
ID uint64 `json:"id"`
Pid int64 `json:"pid"`
Title string `json:"title"`
Path string `json:"path,omitempty"`
ComponentPath string `json:"component_path,omitempty"`
Icon string `json:"icon,omitempty"`
Sort int64 `json:"sort"`
Status int8 `json:"status"`
IsVisible *int8 `json:"is_visible,omitempty"`
Views []int `json:"views,omitempty"`
Type int8 `json:"type"`
Permission string `json:"permission,omitempty"`
Children []*menuNode `json:"children,omitempty"`
}
func buildMenuTree(menus []models.SystemMenu, pid int64) []*menuNode {
var tree []*menuNode
for _, m := range menus {
if m.Pid == pid {
node := &menuNode{
ID: m.ID,
Pid: m.Pid,
Title: m.Title,
Sort: m.Sort,
Status: m.Status,
IsVisible: m.IsVisible,
Views: parseViews(m.Views),
Type: m.Type,
}
if m.Path != nil {
node.Path = *m.Path
}
if m.ComponentPath != nil {
node.ComponentPath = *m.ComponentPath
}
if m.Icon != nil {
node.Icon = *m.Icon
}
if m.Permission != nil {
node.Permission = *m.Permission
}
if children := buildMenuTree(menus, int64(m.ID)); len(children) > 0 {
node.Children = children
}
tree = append(tree, node)
}
}
return tree
}
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"}
_ = c.ServeJSON()
return
}
var body struct {
Status *int8 `json:"status"`
}
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
}
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()
}
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: &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, "")),
}
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.ServeJSON()
}
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"}
_ = c.ServeJSON()
return
}
payload, ok := c.parseMenuPayload(false)
if !ok {
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, "")),
"path": valueString(payload.Path, ""),
"component_path": valueString(payload.ComponentPath, ""),
"icon": valueString(payload.Icon, ""),
"sort": valueInt64(payload.Sort, 0),
"status": valueInt8(payload.Status, 1),
"is_visible": valueInt8(payload.IsVisible, 1),
"views": string(viewsBytes),
"type": valueInt8(payload.Type, 1),
"permission": valueString(payload.Permission, ""),
}
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()
}
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"}
_ = c.ServeJSON()
return
}
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 *BackendMenuController) parseMenuPayload(needTitle bool) (*menuPayload, bool) {
var payload menuPayload
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
}
if needTitle && strings.TrimSpace(valueString(payload.Title, "")) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "菜单名称不能为空"}
_ = c.ServeJSON()
return nil, false
}
return &payload, true
}
func valueString(v *string, def string) string {
if v == nil {
return def
}
return *v
}
func valueInt8(v *int8, def int8) int8 {
if v == nil {
return def
}
return *v
}
func valueInt64(v *int64, def int64) int64 {
if v == nil {
return def
}
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()
}
File diff suppressed because it is too large Load Diff
+303
View File
@@ -0,0 +1,303 @@
package controllers
import (
"encoding/json"
"io"
"strconv"
"strings"
"server/models"
"server/services"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformAdminUserController 平台管理员用户管理(yz_system_admin_user
type PlatformAdminUserController struct {
beego.Controller
}
type adminUserDTO struct {
ID uint64 `json:"id"`
Account string `json:"account"`
Name *string `json:"name"`
Phone *string `json:"phone"`
Email *string `json:"email"`
Qq *string `json:"qq"`
Sex uint8 `json:"sex"`
Avatar *string `json:"avatar"`
Rid uint64 `json:"rid"`
LoginCount uint64 `json:"login_count"`
LastLoginIP *string `json:"last_login_ip"`
Status uint8 `json:"status"`
CreateTime string `json:"create_time"`
UpdateTime *string `json:"update_time"`
}
func toAdminUserDTO(u models.AdminUser) adminUserDTO {
var updateTime *string
if u.UpdateTime != nil {
s := u.UpdateTime.Format("2006-01-02 15:04:05")
updateTime = &s
}
return adminUserDTO{
ID: u.ID,
Account: u.Account,
Name: u.Name,
Phone: u.Phone,
Email: u.Email,
Qq: u.Qq,
Sex: u.Sex,
Avatar: u.Avatar,
Rid: u.RoleID,
LoginCount: u.LoginCount,
LastLoginIP: u.LastLoginIP,
Status: u.Status,
CreateTime: u.CreateTime.Format("2006-01-02 15:04:05"),
UpdateTime: updateTime,
}
}
// GetAllUsers 获取全部平台管理员用户
// GET /platform/getAllUsers
func (c *PlatformAdminUserController) GetAllUsers() {
rows, total, err := services.ListAdminUsers()
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败"}
_ = c.ServeJSON()
return
}
list := make([]adminUserDTO, 0, len(rows))
for _, u := range rows {
list = append(list, toAdminUserDTO(u))
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"list": list, "total": total},
}
_ = c.ServeJSON()
}
// GetUserInfo 获取用户详情
// GET /platform/getUserInfo/:id
func (c *PlatformAdminUserController) GetUserInfo() {
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
}
u, err := services.GetAdminUserByID(id)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "用户不存在"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": toAdminUserDTO(*u),
}
_ = c.ServeJSON()
}
type adminAddUserPayload struct {
Account string `json:"account"`
Password string `json:"password"`
Name *string `json:"name"`
Phone *string `json:"phone"`
Email *string `json:"email"`
Qq *string `json:"qq"`
Sex *uint8 `json:"sex"`
Avatar *string `json:"avatar"`
Rid *uint64 `json:"rid"`
Status *uint8 `json:"status"`
}
// AddUser 添加平台管理员用户(仅写 yz_system_admin_user,不处理 tid
// POST /platform/addUser
func (c *PlatformAdminUserController) AddUser() {
var p adminAddUserPayload
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
}
p.Account = strings.TrimSpace(p.Account)
p.Password = strings.TrimSpace(p.Password)
if p.Account == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "account 不能为空"}
_ = c.ServeJSON()
return
}
if p.Password == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "password 不能为空"}
_ = c.ServeJSON()
return
}
status := uint8(1)
if p.Status != nil {
status = *p.Status
}
sex := uint8(0)
if p.Sex != nil {
sex = *p.Sex
}
roleID := uint64(1)
if p.Rid != nil && *p.Rid != 0 {
roleID = *p.Rid
}
id, err := services.CreateAdminUser(p.Account, p.Password, p.Name, p.Phone, p.Email, p.Qq, p.Avatar, sex, roleID, status)
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{}{"id": id},
}
_ = c.ServeJSON()
}
type editUserPayload struct {
Account *string `json:"account"`
Password *string `json:"password"`
Name *string `json:"name"`
Phone *string `json:"phone"`
Email *string `json:"email"`
Qq *string `json:"qq"`
Sex *uint8 `json:"sex"`
Avatar *string `json:"avatar"`
Rid *uint64 `json:"rid"`
Status *uint8 `json:"status"`
}
// EditUser 编辑用户信息(password 可选,存在则修改)
// POST /platform/editUser/:id
func (c *PlatformAdminUserController) 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
}
var p editUserPayload
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
}
fields := map[string]interface{}{}
if p.Account != nil {
acc := strings.TrimSpace(*p.Account)
if acc != "" {
fields["account"] = acc
}
}
if p.Name != nil {
fields["name"] = *p.Name
}
if p.Phone != nil {
fields["phone"] = *p.Phone
}
if p.Email != nil {
fields["email"] = *p.Email
}
if p.Qq != nil {
fields["qq"] = *p.Qq
}
if p.Sex != nil {
fields["sex"] = *p.Sex
}
if p.Avatar != nil {
fields["avatar"] = *p.Avatar
}
if p.Rid != nil && *p.Rid != 0 {
fields["role_id"] = *p.Rid
}
if p.Status != nil {
fields["status"] = *p.Status
}
if len(fields) > 0 {
if err := services.UpdateAdminUser(id, fields); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "编辑失败"}
_ = c.ServeJSON()
return
}
}
if p.Password != nil && strings.TrimSpace(*p.Password) != "" {
if err := services.ChangeAdminUserPassword(id, *p.Password); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "密码修改失败"}
_ = c.ServeJSON()
return
}
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// DeleteUser 删除用户
// DELETE /platform/deleteUser/:id
func (c *PlatformAdminUserController) 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
}
if err := services.DeleteAdminUser(id); err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
type changePasswordPayload struct {
ID uint64 `json:"id"`
Password string `json:"password"`
}
// ChangePassword 修改密码
// POST /platform/changePassword
func (c *PlatformAdminUserController) ChangePassword() {
var p changePasswordPayload
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
}
if err := services.ChangeAdminUserPassword(p.ID, p.Password); 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()
}
+425
View File
@@ -0,0 +1,425 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strings"
"server/models"
"server/pkg/jwtutil"
"server/services"
beego "github.com/beego/beego/v2/server/web"
)
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 {
TenantName string `json:"tenant_name"`
Account string `json:"account"`
Password string `json:"password"`
Code string `json:"code"`
}
// PlatformAuthController 平台端认证控制器
type PlatformAuthController struct {
beego.Controller
}
// LoginPlatform 平台端登录(不需要租户)
func (c *PlatformAuthController) LoginPlatform() {
var req platformLoginRequest
// 先尝试从缓存读取
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": "参数错误",
}
_ = 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": "参数错误: " + 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": "用户名或密码不能为空",
}
_ = c.ServeJSON()
return
}
cfg, _ := models.GetPlatformLoginVerify()
if cfg.OpenVerifyEnabled == 1 {
// 极验验证
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()
return
}
if err := services.VerifyPlatformLoginCode(req.Account, cfg.VerifyType, req.Code); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
_ = c.ServeJSON()
return
}
}
}
// 控制器只做 HTTP 解析与响应编排,业务逻辑放 services 层
token, loginUser, err := services.PlatformAdminLogin(req.Account, req.Password)
if err != nil {
c.Data["json"] = map[string]interface{}{
"code": 401,
"msg": err.Error(),
}
_ = c.ServeJSON()
return
}
c.Data["json"] = 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,
"rid": loginUser.Rid,
"avatar": loginUser.Avatar,
"role_name": loginUser.RoleName,
},
},
}
_ = c.ServeJSON()
}
// LoginBackend backend 登录(需要租户)
func (c *PlatformAuthController) LoginBackend() {
var req backendLoginRequest
body := c.Ctx.Input.RequestBody
if len(body) == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
if err := json.Unmarshal(body, &req); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
if req.TenantName == "" || req.Account == "" || req.Password == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户名称、用户名或密码不能为空"}
_ = c.ServeJSON()
return
}
cfg, _ := models.GetPlatformLoginVerify()
if cfg.OpenVerifyEnabled == 1 {
if cfg.VerifyType == "sms" || cfg.VerifyType == "email" {
if strings.TrimSpace(req.Code) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "请输入验证码"}
_ = c.ServeJSON()
return
}
if err := services.VerifyBackendLoginCode(req.TenantName, req.Account, cfg.VerifyType, req.Code); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
_ = c.ServeJSON()
return
}
}
}
token, loginUser, err := services.BackendLogin(req.TenantName, req.Account, req.Password)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": err.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = 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,
},
},
}
_ = c.ServeJSON()
}
// GetCurrentUser 当前登录平台用户信息(含角色名称),需 Bearer Token
func (c *PlatformAuthController) GetCurrentUser() {
authHeader := c.Ctx.Request.Header.Get("Authorization")
if authHeader == "" {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "未登录"}
_ = c.ServeJSON()
return
}
authParts := strings.SplitN(authHeader, " ", 2)
if len(authParts) != 2 || authParts[0] != "Bearer" {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "认证信息格式错误"}
_ = c.ServeJSON()
return
}
claims, err := jwtutil.ParseToken(authParts[1])
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "无效的token"}
_ = c.ServeJSON()
return
}
if claims.UserType != "platform" {
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "无权访问"}
_ = c.ServeJSON()
return
}
loginUser, err := services.PlatformGetCurrentUser(uint64(claims.UserID))
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 401, "msg": err.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"id": loginUser.ID,
"account": loginUser.Account,
"name": loginUser.Name,
"rid": loginUser.Rid,
"avatar": loginUser.Avatar,
"role_name": loginUser.RoleName,
},
}
_ = c.ServeJSON()
}
// SendLoginCode 发送登录验证码(占位实现)
func (c *PlatformAuthController) SendLoginCode() {
var req struct {
Account string `json:"account"`
TenantName string `json:"tenant_name"`
Channel string `json:"channel"`
}
body := c.Ctx.Input.RequestBody
if err := json.Unmarshal(body, &req); err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
_ = c.ServeJSON()
return
}
cfg, _ := models.GetPlatformLoginVerify()
if cfg.OpenVerifyEnabled != 1 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "当前未开启验证"}
_ = c.ServeJSON()
return
}
channel := strings.TrimSpace(req.Channel)
if channel == "" {
channel = cfg.VerifyType
}
if channel != "sms" && channel != "email" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "仅支持短信/邮箱验证码"}
_ = c.ServeJSON()
return
}
path := strings.ToLower(c.Ctx.Request.URL.Path)
var sendErr error
if strings.HasPrefix(path, "/backend/") {
sendErr = services.SendBackendLoginCode(req.TenantName, req.Account, channel)
} else {
sendErr = services.SendPlatformLoginCode(req.Account, channel)
}
if sendErr != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": sendErr.Error()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "验证码已发送"}
_ = c.ServeJSON()
}
// LoginBySms 手机号验证码登录(占位实现)
func (c *PlatformAuthController) LoginBySms() {
c.Data["json"] = map[string]interface{}{
"code": 501,
"msg": "手机号验证码登录暂未实现",
}
_ = c.ServeJSON()
}
// Logout 平台退出登录(占位实现,当前为无状态直接返回成功)
func (c *PlatformAuthController) Logout() {
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "退出成功",
}
_ = c.ServeJSON()
}
// GetGeetest3Infos 获取极验3.0配置(占位实现)
func (c *PlatformAuthController) GetGeetest3Infos() {
cfg, _ := models.GetPlatformLoginVerify()
if cfg.Geetest3ID == nil || cfg.Geetest3Key == nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "未配置极验3参数"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"captcha_id": *cfg.Geetest3ID,
"captcha_key": *cfg.Geetest3Key,
},
}
_ = c.ServeJSON()
}
// GetGeetest4Infos 获取极验4.0配置(占位实现)
func (c *PlatformAuthController) GetGeetest4Infos() {
cfg, _ := models.GetPlatformLoginVerify()
if cfg.Geetest4ID == nil || cfg.Geetest4Key == nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "未配置极验4参数"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"captcha_id": *cfg.Geetest4ID,
"captcha_key": *cfg.Geetest4Key,
},
}
_ = c.ServeJSON()
}
// GetOpenVerify 判断是否开启登录验证(占位实现)
func (c *PlatformAuthController) GetOpenVerify() {
cfg, _ := models.GetPlatformLoginVerify()
openVerify := "0"
if cfg.OpenVerifyEnabled == 1 {
openVerify = "1"
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "ok",
"data": []map[string]string{
{
"label": "openVerify",
"value": openVerify,
},
{
"label": "verifyType",
"value": cfg.VerifyType,
},
},
}
_ = c.ServeJSON()
}
// Register 注册(占位实现)
func (c *PlatformAuthController) Register() {
c.Data["json"] = map[string]interface{}{
"code": 501,
"msg": "注册暂未实现",
}
_ = c.ServeJSON()
}
// SendRegisterCode 发送注册验证码(占位实现)
func (c *PlatformAuthController) SendRegisterCode() {
c.Data["json"] = map[string]interface{}{
"code": 501,
"msg": "发送注册验证码暂未实现",
}
_ = c.ServeJSON()
}
// ResetPassword 忘记密码重置(占位实现)
func (c *PlatformAuthController) ResetPassword() {
c.Data["json"] = map[string]interface{}{
"code": 501,
"msg": "重置密码暂未实现",
}
_ = c.ServeJSON()
}
// SendResetCode 发送找回密码验证码(占位实现)
func (c *PlatformAuthController) SendResetCode() {
c.Data["json"] = map[string]interface{}{
"code": 501,
"msg": "发送找回密码验证码暂未实现",
}
_ = c.ServeJSON()
}
+363
View File
@@ -0,0 +1,363 @@
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"
)
type PlatformComplaintController struct {
beego.Controller
}
func (c *PlatformComplaintController) 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 *PlatformComplaintController) 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 *PlatformComplaintController) ok(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
func categoryNameMap(ids []uint64) map[uint64]string {
m := make(map[uint64]string)
if len(ids) == 0 {
return m
}
seen := make(map[uint64]bool)
var uniq []uint64
for _, id := range ids {
if id > 0 && !seen[id] {
seen[id] = true
uniq = append(uniq, id)
}
}
if len(uniq) == 0 {
return m
}
var cats []models.ComplaintCategory
_, _ = models.Orm.QueryTable(new(models.ComplaintCategory)).
Filter("id__in", uniq).
Filter("delete_time__isnull", true).
All(&cats)
for _, x := range cats {
m[x.ID] = x.Name
}
return m
}
// List GET /platform/complaint/list?page=1&pageSize=20&categoryId=&status=&keyword=
func (c *PlatformComplaintController) 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
}
var categoryID uint64
if s := strings.TrimSpace(c.GetString("categoryId")); s != "" {
if v, err := strconv.ParseUint(s, 10, 64); err == nil {
categoryID = v
}
}
statusStr := strings.TrimSpace(c.GetString("status"))
keyword := strings.TrimSpace(c.GetString("keyword"))
qs := models.Orm.QueryTable(new(models.PlatformComplaint)).Filter("delete_time__isnull", true)
if categoryID > 0 {
qs = qs.Filter("category_id", categoryID)
}
if statusStr != "" {
if st, err := strconv.Atoi(statusStr); err == nil {
qs = qs.Filter("status", st)
}
}
if keyword != "" {
cond := orm.NewCondition().
Or("title__icontains", keyword).
Or("content__icontains", keyword).
Or("contact_name__icontains", keyword).
Or("contact_phone__icontains", keyword).
Or("contact_email__icontains", keyword)
qs = qs.SetCond(cond)
}
total, _ := qs.Count()
var rows []models.PlatformComplaint
_, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows)
if err != nil {
c.jsonErr(500, 500, "获取失败: "+err.Error())
return
}
ids := make([]uint64, 0, len(rows))
for _, r := range rows {
ids = append(ids, r.CategoryID)
}
names := categoryNameMap(ids)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
list = append(list, map[string]interface{}{
"id": r.ID,
"categoryId": r.CategoryID,
"categoryName": names[r.CategoryID],
"title": r.Title,
"content": r.Content,
"contactName": r.ContactName,
"contactPhone": r.ContactPhone,
"contactEmail": r.ContactEmail,
"status": r.Status,
"replyContent": r.ReplyContent,
"replyTime": r.ReplyTime,
"tid": r.Tid,
"remark": r.Remark,
"createTime": r.CreateTime,
"updateTime": r.UpdateTime,
})
}
c.ok(map[string]interface{}{
"list": list,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
// Detail GET /platform/complaint/:id
func (c *PlatformComplaintController) 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.PlatformComplaint
err = models.Orm.QueryTable(new(models.PlatformComplaint)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&row)
if err != nil {
c.jsonErr(404, 404, "记录不存在")
return
}
names := categoryNameMap([]uint64{row.CategoryID})
c.ok(map[string]interface{}{
"id": row.ID,
"categoryId": row.CategoryID,
"categoryName": names[row.CategoryID],
"title": row.Title,
"content": row.Content,
"contactName": row.ContactName,
"contactPhone": row.ContactPhone,
"contactEmail": row.ContactEmail,
"status": row.Status,
"replyContent": row.ReplyContent,
"replyTime": row.ReplyTime,
"tid": row.Tid,
"remark": row.Remark,
"createTime": row.CreateTime,
"updateTime": row.UpdateTime,
})
}
type complaintPayload struct {
CategoryID *uint64 `json:"categoryId"`
Title *string `json:"title"`
Content *string `json:"content"`
ContactName *string `json:"contactName"`
ContactPhone *string `json:"contactPhone"`
ContactEmail *string `json:"contactEmail"`
Status *int8 `json:"status"`
ReplyContent *string `json:"replyContent"`
Tid *uint64 `json:"tid"`
Remark *string `json:"remark"`
}
// Create POST /platform/complaint
func (c *PlatformComplaintController) Create() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
body, _ := io.ReadAll(c.Ctx.Request.Body)
var p complaintPayload
if err := json.Unmarshal(body, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
if p.CategoryID == nil || *p.CategoryID == 0 || p.Title == nil || strings.TrimSpace(*p.Title) == "" ||
p.Content == nil || strings.TrimSpace(*p.Content) == "" {
c.jsonErr(400, 400, "分类、标题、内容不能为空")
return
}
row := models.PlatformComplaint{
CategoryID: *p.CategoryID,
Title: strings.TrimSpace(*p.Title),
Content: strings.TrimSpace(*p.Content),
Status: 0,
}
if p.ContactName != nil {
row.ContactName = p.ContactName
}
if p.ContactPhone != nil {
row.ContactPhone = p.ContactPhone
}
if p.ContactEmail != nil {
row.ContactEmail = p.ContactEmail
}
if p.Tid != nil {
row.Tid = p.Tid
}
if p.Status != nil {
row.Status = *p.Status
}
if p.Remark != nil {
row.Remark = p.Remark
}
id, err := models.Orm.Insert(&row)
if err != nil {
c.jsonErr(500, 500, "创建失败: "+err.Error())
return
}
c.ok(map[string]interface{}{"id": id})
}
// Update POST /platform/complaint/:id
func (c *PlatformComplaintController) Update() {
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
}
body, _ := io.ReadAll(c.Ctx.Request.Body)
var p complaintPayload
if err := json.Unmarshal(body, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
up := map[string]interface{}{}
if p.CategoryID != nil && *p.CategoryID > 0 {
up["category_id"] = *p.CategoryID
}
if p.Title != nil {
up["title"] = strings.TrimSpace(*p.Title)
}
if p.Content != nil {
up["content"] = strings.TrimSpace(*p.Content)
}
if p.ContactName != nil {
up["contact_name"] = p.ContactName
}
if p.ContactPhone != nil {
up["contact_phone"] = p.ContactPhone
}
if p.ContactEmail != nil {
up["contact_email"] = p.ContactEmail
}
if p.Status != nil {
up["status"] = *p.Status
}
if p.ReplyContent != nil {
s := strings.TrimSpace(*p.ReplyContent)
up["reply_content"] = s
if s != "" {
now := time.Now()
up["reply_time"] = now
}
}
if p.Tid != nil {
up["tid"] = p.Tid
}
if p.Remark != nil {
up["remark"] = p.Remark
}
if len(up) == 0 {
c.jsonErr(400, 400, "无更新字段")
return
}
n, err := models.Orm.QueryTable(new(models.PlatformComplaint)).
Filter("id", id).
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.ok(nil)
}
// Delete DELETE /platform/complaint/:id
func (c *PlatformComplaintController) 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.PlatformComplaint)).
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)
}
@@ -0,0 +1,203 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
type PlatformComplaintCategoryController struct {
beego.Controller
}
func (c *PlatformComplaintCategoryController) 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 *PlatformComplaintCategoryController) 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 *PlatformComplaintCategoryController) ok(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
// List GET /platform/complaintCategory/list
func (c *PlatformComplaintCategoryController) List() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var rows []models.ComplaintCategory
_, err := models.Orm.QueryTable(new(models.ComplaintCategory)).
Filter("delete_time__isnull", true).
OrderBy("sort", "id").
All(&rows)
if err != nil {
c.jsonErr(500, 500, "获取失败: "+err.Error())
return
}
c.ok(rows)
}
// SelectList GET /platform/complaintCategory/select — 仅启用,供下拉
func (c *PlatformComplaintCategoryController) SelectList() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var rows []models.ComplaintCategory
_, err := models.Orm.QueryTable(new(models.ComplaintCategory)).
Filter("delete_time__isnull", true).
Filter("status", 1).
OrderBy("sort", "id").
All(&rows)
if err != nil {
c.jsonErr(500, 500, "获取失败: "+err.Error())
return
}
c.ok(rows)
}
type complaintCategoryPayload struct {
Name *string `json:"name"`
Code *string `json:"code"`
Sort *int `json:"sort"`
Status *int8 `json:"status"`
}
// Create POST /platform/complaintCategory
func (c *PlatformComplaintCategoryController) Create() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
body, _ := io.ReadAll(c.Ctx.Request.Body)
var p complaintCategoryPayload
if err := json.Unmarshal(body, &p); err != nil || p.Name == nil || strings.TrimSpace(*p.Name) == "" {
c.jsonErr(400, 400, "分类名称不能为空")
return
}
sort := 0
if p.Sort != nil {
sort = *p.Sort
}
st := int8(1)
if p.Status != nil {
st = *p.Status
}
row := models.ComplaintCategory{
Name: strings.TrimSpace(*p.Name),
Code: p.Code,
Sort: sort,
Status: st,
}
id, err := models.Orm.Insert(&row)
if err != nil {
c.jsonErr(500, 500, "创建失败: "+err.Error())
return
}
c.ok(map[string]interface{}{"id": id})
}
// Update POST /platform/complaintCategory/:id
func (c *PlatformComplaintCategoryController) Update() {
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
}
body, _ := io.ReadAll(c.Ctx.Request.Body)
var p complaintCategoryPayload
if err := json.Unmarshal(body, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
up := map[string]interface{}{}
if p.Name != nil {
up["name"] = strings.TrimSpace(*p.Name)
}
if p.Code != nil {
up["code"] = strings.TrimSpace(*p.Code)
}
if p.Sort != nil {
up["sort"] = *p.Sort
}
if p.Status != nil {
up["status"] = *p.Status
}
if len(up) == 0 {
c.jsonErr(400, 400, "无更新字段")
return
}
n, err := models.Orm.QueryTable(new(models.ComplaintCategory)).
Filter("id", id).
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.ok(nil)
}
// Delete DELETE /platform/complaintCategory/:id
func (c *PlatformComplaintCategoryController) 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.ComplaintCategory)).
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)
}
+615
View File
@@ -0,0 +1,615 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformDomainPoolController 主域名池管理
type PlatformDomainPoolController struct {
beego.Controller
}
// PlatformTenantDomainController 租户域名管理
type PlatformTenantDomainController struct {
beego.Controller
}
func requirePlatform(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 != "platform" {
return nil, fmt.Errorf("无权访问")
}
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=
func (c *PlatformDomainPoolController) Index() {
if _, err := requirePlatform(&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 /platform/domain/pool/getEnabledDomains
func (c *PlatformDomainPoolController) GetEnabledDomains() {
if _, err := requirePlatform(&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()
}
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 {
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 /platform/domain/pool/update
func (c *PlatformDomainPoolController) Update() {
if _, err := requirePlatform(&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 /platform/domain/pool/delete/:id
func (c *PlatformDomainPoolController) Delete() {
if _, err := requirePlatform(&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 /platform/domain/pool/toggleStatus body:{id}
func (c *PlatformDomainPoolController) ToggleStatus() {
if _, err := requirePlatform(&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 /platform/domain/tenant/index?page=&pageSize=&tid=&status=&sub_domain=
func (c *PlatformTenantDomainController) Index() {
if _, err := requirePlatform(&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 /platform/domain/tenant/myDomains?tid=1
func (c *PlatformTenantDomainController) MyDomains() {
if _, err := requirePlatform(&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()
}
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 {
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 /platform/domain/tenant/audit body:{id,action} action=approve/reject
func (c *PlatformTenantDomainController) Audit() {
if _, err := requirePlatform(&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 /platform/domain/tenant/toggleStatus body:{id}
func (c *PlatformTenantDomainController) ToggleStatus() {
if _, err := requirePlatform(&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 /platform/domain/tenant/delete/:id
func (c *PlatformTenantDomainController) Delete() {
if _, err := requirePlatform(&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
+265
View File
@@ -0,0 +1,265 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"server/models"
"server/pkg/jwtutil"
"server/services"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformEmailController 系统邮箱配置(yz_system_email
type PlatformEmailController struct {
beego.Controller
}
func (c *PlatformEmailController) 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 *PlatformEmailController) jsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func emailRowToMap(m models.SystemEmail) map[string]interface{} {
out := map[string]interface{}{
"id": m.ID,
"from_address": m.FromAddress,
"host": m.Host,
"port": m.Port,
"password": m.Password,
"encryption": m.Encryption,
"timeout": m.Timeout,
"status": m.Status,
"create_time": m.CreateTime.Format("2006-01-02 15:04:05"),
"update_time": m.UpdateTime.Format("2006-01-02 15:04:05"),
}
if m.FromName != nil {
out["from_name"] = *m.FromName
} else {
out["from_name"] = ""
}
if m.Remark != nil {
out["remark"] = *m.Remark
} else {
out["remark"] = ""
}
return out
}
// GetInfo GET /platform/email/info
func (c *PlatformEmailController) GetInfo() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
rows, err := services.ListSystemEmails()
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, emailRowToMap(rows[i]))
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": list}
_ = c.ServeJSON()
}
type emailFormPayload struct {
FromAddress string `json:"fromAddress"`
FromName string `json:"fromName"`
Host string `json:"host"`
Port interface{} `json:"port"`
Password string `json:"password"`
Encryption string `json:"encryption"`
Timeout interface{} `json:"timeout"`
}
type testEmailPayload struct {
emailFormPayload
TestEmail string `json:"testEmail"`
}
func parseUintFlexible(v interface{}) uint {
if v == nil {
return 0
}
switch x := v.(type) {
case float64:
if x < 0 {
return 0
}
return uint(x)
case string:
n, err := parseUintString(x)
if err != nil {
return 0
}
return n
default:
return 0
}
}
func parseUintString(s string) (uint, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, fmt.Errorf("empty")
}
n, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return 0, err
}
return uint(n), nil
}
func normalizeEncryption(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
switch s {
case "ssl", "tls", "none":
return s
default:
return "ssl"
}
}
// EditInfo POST /platform/email/editinfo
func (c *PlatformEmailController) EditInfo() {
if _, err := c.platformClaims(); 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 emailFormPayload
if uerr := json.Unmarshal(raw, &p); uerr != nil {
c.jsonErr(400, 400, "参数错误")
return
}
from := strings.TrimSpace(p.FromAddress)
host := strings.TrimSpace(p.Host)
if from == "" || host == "" {
c.jsonErr(400, 400, "发件人邮箱与 SMTP 主机不能为空")
return
}
port := parseUintFlexible(p.Port)
if port == 0 {
port = 465
}
timeout := parseUintFlexible(p.Timeout)
if timeout == 0 {
timeout = 30
}
enc := normalizeEncryption(p.Encryption)
cnt, cerr := models.Orm.QueryTable(new(models.SystemEmail)).Count()
if cerr != nil {
c.jsonErr(500, 500, "读取邮箱配置失败: "+cerr.Error())
return
}
if strings.TrimSpace(p.Password) == "" && cnt == 0 {
c.jsonErr(400, 400, "授权码/密码不能为空")
return
}
var fn *string
if strings.TrimSpace(p.FromName) != "" {
s := strings.TrimSpace(p.FromName)
fn = &s
}
err = services.UpsertFirstSystemEmail(from, fn, host, port, strings.TrimSpace(p.Password), enc, timeout, 1, nil)
if err != nil {
c.jsonErr(500, 500, "保存邮箱配置失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功"}
_ = c.ServeJSON()
}
// SendTestEmail POST /platform/email/sendtestemail
func (c *PlatformEmailController) SendTestEmail() {
if _, err := c.platformClaims(); 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 testEmailPayload
if uerr := json.Unmarshal(raw, &p); uerr != nil {
c.jsonErr(400, 400, "参数错误")
return
}
to := strings.TrimSpace(p.TestEmail)
if to == "" {
c.jsonErr(400, 400, "测试收件邮箱不能为空")
return
}
from := strings.TrimSpace(p.FromAddress)
host := strings.TrimSpace(p.Host)
if from == "" || host == "" {
c.jsonErr(400, 400, "发件人邮箱与 SMTP 主机不能为空")
return
}
port := parseUintFlexible(p.Port)
if port == 0 {
port = 465
}
timeout := parseUintFlexible(p.Timeout)
if timeout == 0 {
timeout = 30
}
enc := normalizeEncryption(p.Encryption)
pass := strings.TrimSpace(p.Password)
if pass == "" {
rows, lerr := services.ListSystemEmails()
if lerr == nil && len(rows) > 0 {
pass = rows[0].Password
}
}
if pass == "" {
c.jsonErr(400, 400, "授权码/密码不能为空(请填写或先保存配置)")
return
}
cfg := services.SMTPConfig{
FromAddress: from,
FromName: strings.TrimSpace(p.FromName),
Host: host,
Port: port,
Password: pass,
Encryption: enc,
Timeout: timeout,
}
if err := services.SendTestEmailSMTP(cfg, to); err != nil {
c.jsonErr(500, 500, "发送失败: "+err.Error())
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "发送成功"}
_ = c.ServeJSON()
}
+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"
)
// PlatformFileController 平台端文件管理(yz_system_files / yz_system_files_category
type PlatformFileController struct {
beego.Controller
}
const platformFileUploadMaxMB = 2048 // 2GB,适用于大型软件安装包
const platformFileUploadMaxBytes = platformFileUploadMaxMB * 1024 * 1024
var platformFileTypeByCategory = map[string]uint8{
"image": 1,
"document": 2,
"video": 3,
"audio": 4,
"appsupgrade": 2,
}
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"},
"audio": {"mp3", "wav", "ogg"},
// 安装包 / 软件升级(上传时 cate 选 appsupgrade 分类即可,扩展名在此放行)
"appsupgrade": {"zip", "exe", "dmg", "msi", "msix", "apk", "deb", "rpm", "7z", "tar", "gz", "pkg"},
}
func (c *PlatformFileController) 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 *PlatformFileController) 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 *PlatformFileController) 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 *PlatformFileController) jsonOK(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
func platformDetectFileType(ext string) uint8 {
ext = strings.ToLower(strings.TrimPrefix(ext, "."))
for cat, exts := range platformAllowedExtByCategory {
for _, e := range exts {
if e == ext {
if t, ok := platformFileTypeByCategory[cat]; ok {
return t
}
return 2
}
}
}
return 2
}
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:])
}
return ""
}
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,
"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 platformRemovePhysicalBySrc(webSrc string) {
webSrc = strings.TrimSpace(webSrc)
if webSrc == "" {
return
}
webSrc = strings.TrimPrefix(webSrc, "/")
_ = os.Remove(webSrc)
}
// GetAllFiles GET /platform/allfiles
func (c *PlatformFileController) GetAllFiles() {
claims, err := c.platformClaims()
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, platformFileToMap(&rows[i]))
}
c.jsonOK(map[string]interface{}{
"list": list,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
// GetUserCate GET /platform/usercate
func (c *PlatformFileController) GetUserCate() {
claims, err := c.platformClaims()
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 platformCreateCateBody struct {
Name string `json:"name"`
Tuid *uint64 `json:"tuid"`
}
// CreateFileCate POST /platform/createfilecate
func (c *PlatformFileController) CreateFileCate() {
claims, err := c.platformClaims()
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 platformCreateCateBody
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 platformRenameCateBody struct {
Name string `json:"name"`
}
// RenameFileCate POST /platform/renamefilecate/:id
func (c *PlatformFileController) RenameFileCate() {
claims, err := c.platformClaims()
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 platformRenameCateBody
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 /platform/deletefilecate/:id
func (c *PlatformFileController) DeleteFileCate() {
claims, err := c.platformClaims()
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 /platform/catefiles/:id
func (c *PlatformFileController) GetCateFiles() {
claims, err := c.platformClaims()
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, platformFileToMap(&rows[i]))
}
c.jsonOK(map[string]interface{}{
"list": list,
"total": total,
"page": page,
"pageSize": pageSize,
"categoryId": cateID,
})
}
// GetFileByID GET /platform/file/:id
func (c *PlatformFileController) GetFileByID() {
claims, err := c.platformClaims()
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(platformFileToMap(&f))
}
// UploadFile POST /platform/uploadfile
func (c *PlatformFileController) UploadFile() {
claims, err := c.platformClaims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
tid := c.effectiveTid(claims)
if err := c.Ctx.Request.ParseMultipartForm(platformFileUploadMaxBytes); 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 > platformFileUploadMaxBytes {
c.jsonErr(400, 400, fmt.Sprintf("文件大小不能超过%dMB", platformFileUploadMaxMB))
return
}
ext := platformFileExt(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: platformDetectFileType(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 platformMd5HashFile(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 platformUpdateFileBody struct {
Name *string `json:"name"`
Cate *uint64 `json:"cate"`
}
// UpdateFile POST /platform/updatefile/:id
func (c *PlatformFileController) UpdateFile() {
claims, err := c.platformClaims()
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 platformUpdateFileBody
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 /platform/deletefile/:id
func (c *PlatformFileController) DeleteFile() {
claims, err := c.platformClaims()
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 /platform/deletefilepermanently/:id
func (c *PlatformFileController) DeleteFilePermanently() {
claims, err := c.platformClaims()
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
}
platformRemovePhysicalBySrc(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 /platform/movefile/:id
func (c *PlatformFileController) MoveFile() {
claims, err := c.platformClaims()
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 platformIdsBody struct {
IDs []uint64 `json:"ids"`
Cate *uint64 `json:"cate"`
}
// BatchDeleteFiles POST /platform/batchdeletefiles
func (c *PlatformFileController) BatchDeleteFiles() {
claims, err := c.platformClaims()
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 platformIdsBody
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 != "" {
platformRemovePhysicalBySrc(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 /platform/batchDeleteFilesPermanently
func (c *PlatformFileController) BatchDeleteFilesPermanently() {
claims, err := c.platformClaims()
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 platformIdsBody
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 {
platformRemovePhysicalBySrc(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 /platform/uploadavatar(占位)
func (c *PlatformFileController) UploadAvatar() {
c.Data["json"] = map[string]interface{}{"code": 501, "msg": "上传头像暂未实现"}
_ = c.ServeJSON()
}
// UpdateAvatar POST /platform/uploadavatar/:id(占位)
func (c *PlatformFileController) UpdateAvatar() {
c.Data["json"] = map[string]interface{}{"code": 501, "msg": "更新头像暂未实现"}
_ = c.ServeJSON()
}
// BatchMoveFiles POST /platform/batchMoveFiles
func (c *PlatformFileController) BatchMoveFiles() {
claims, err := c.platformClaims()
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 platformIdsBody
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()
}
+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()
}
+124
View File
@@ -0,0 +1,124 @@
package controllers
import (
"encoding/json"
"io"
"strings"
"server/models"
beego "github.com/beego/beego/v2/server/web"
)
type PlatformLoginVerifyController struct {
beego.Controller
}
type loginVerifyPayload struct {
OpenVerifyEnabled *int8 `json:"openVerify_enabled"`
VerifyType string `json:"use_geetest"`
Geetest3ID *string `json:"geetest3_id"`
Geetest3Key *string `json:"geetest3_key"`
Geetest4ID *string `json:"geetest4_id"`
Geetest4Key *string `json:"geetest4_key"`
}
func normalizeVerifyType(v string) string {
switch strings.TrimSpace(v) {
case "sms", "geetest", "email", "captcha":
return strings.TrimSpace(v)
default:
return "captcha"
}
}
// GetLoginVerifyInfos 获取登录验证配置
// GET /platform/loginVerifyInfos
func (c *PlatformLoginVerifyController) GetLoginVerifyInfos() {
cfg, err := models.GetPlatformLoginVerify()
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{}{
"openVerify_enabled": cfg.OpenVerifyEnabled,
"use_geetest": cfg.VerifyType,
"geetest3_id": cfg.Geetest3ID,
"geetest3_key": cfg.Geetest3Key,
"geetest4_id": cfg.Geetest4ID,
"geetest4_key": cfg.Geetest4Key,
},
}
_ = c.ServeJSON()
}
// SaveLoginVerifyInfos 保存登录验证配置
// POST /platform/saveloginVerifyInfos
func (c *PlatformLoginVerifyController) SaveLoginVerifyInfos() {
var p loginVerifyPayload
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
}
verifyType := normalizeVerifyType(p.VerifyType)
openVerifyEnabled := int8(1)
if p.OpenVerifyEnabled != nil {
openVerifyEnabled = *p.OpenVerifyEnabled
}
if verifyType == "geetest" {
if p.Geetest4ID == nil || strings.TrimSpace(*p.Geetest4ID) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "geetest4_id 不能为空"}
_ = c.ServeJSON()
return
}
if p.Geetest4Key == nil || strings.TrimSpace(*p.Geetest4Key) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "geetest4_key 不能为空"}
_ = c.ServeJSON()
return
}
}
var existed models.PlatformLoginVerify
err := models.Orm.QueryTable(new(models.PlatformLoginVerify)).OrderBy("-id").One(&existed)
if err == nil {
update := map[string]interface{}{
"open_verify_enabled": openVerifyEnabled,
"verify_type": verifyType,
"geetest3_id": p.Geetest3ID,
"geetest3_key": p.Geetest3Key,
"geetest4_id": p.Geetest4ID,
"geetest4_key": p.Geetest4Key,
}
_, err = models.Orm.QueryTable(new(models.PlatformLoginVerify)).Filter("id", existed.ID).Update(update)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "保存失败"}
_ = c.ServeJSON()
return
}
} else {
row := &models.PlatformLoginVerify{
OpenVerifyEnabled: openVerifyEnabled,
VerifyType: verifyType,
Geetest3ID: p.Geetest3ID,
Geetest3Key: p.Geetest3Key,
Geetest4ID: p.Geetest4ID,
Geetest4Key: p.Geetest4Key,
}
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()
}
+400
View File
@@ -0,0 +1,400 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformModulesController 模块管理(yz_system_modules
type PlatformModulesController struct {
beego.Controller
}
func (c *PlatformModulesController) modulesClaims() (*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")
}
// 语义更正:
// - /platform/* 只能 platform 访问
// - /backend/* 只能 backend 访问
// 兼容:历史 token 可能缺少 user_type(按 user 处理),此时都拒绝访问以避免越权。
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 (c *PlatformModulesController) jsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
// GetList GET /platform/modules/list
func (c *PlatformModulesController) GetList() {
if _, err := c.modulesClaims(); 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).
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()
}
// GetTenantList GET /platform/modules/getTenantList
// 兼容旧接口命名:返回当前账号可见的模块。当前实现:返回 status=1 且 is_show=1 的全部模块。
func (c *PlatformModulesController) GetTenantList() {
if _, err := c.modulesClaims(); 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()
}
// GetDetail GET /platform/modules/:id
func (c *PlatformModulesController) GetDetail() {
if _, err := c.modulesClaims(); 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, "参数错误")
return
}
var row models.SystemModules
err = models.Orm.QueryTable(new(models.SystemModules)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&row)
if err != nil {
c.jsonErr(404, 404, "模块不存在")
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "获取成功", "data": row}
_ = c.ServeJSON()
}
type modulePayload struct {
Mid *uint64 `json:"mid"`
Name string `json:"name"`
Code string `json:"code"`
Path string `json:"path"`
Icon string `json:"icon"`
Description string `json:"description"`
Type int `json:"type"`
Sort int `json:"sort"`
Status int8 `json:"status"`
IsShow int8 `json:"is_show"`
}
// Add POST /platform/modules
func (c *PlatformModulesController) Add() {
if _, err := c.modulesClaims(); 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 modulePayload
if err := json.Unmarshal(raw, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
p.Name = strings.TrimSpace(p.Name)
p.Code = strings.TrimSpace(p.Code)
if p.Name == "" || p.Code == "" {
c.jsonErr(400, 400, "模块名称和编码不能为空")
return
}
// code 唯一(排除软删)
cnt, _ := models.Orm.QueryTable(new(models.SystemModules)).
Filter("code", p.Code).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
c.jsonErr(400, 400, "模块编码已存在")
return
}
now := time.Now()
row := &models.SystemModules{
Mid: p.Mid,
Name: p.Name,
Code: p.Code,
Path: strings.TrimSpace(p.Path),
Icon: strings.TrimSpace(p.Icon),
Description: strings.TrimSpace(p.Description),
Type: p.Type,
Sort: p.Sort,
Status: p.Status,
IsShow: p.IsShow,
CreateTime: &now,
UpdateTime: &now,
}
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()
}
// Edit PUT /platform/modules/:id
func (c *PlatformModulesController) Edit() {
if _, err := c.modulesClaims(); 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, "参数错误")
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
var p modulePayload
if err := json.Unmarshal(raw, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
p.Name = strings.TrimSpace(p.Name)
p.Code = strings.TrimSpace(p.Code)
if p.Name == "" || p.Code == "" {
c.jsonErr(400, 400, "模块名称和编码不能为空")
return
}
// code 唯一(排除自身与软删)
cnt, _ := models.Orm.QueryTable(new(models.SystemModules)).
Filter("code", p.Code).
Filter("id__ne", id).
Filter("delete_time__isnull", true).
Count()
if cnt > 0 {
c.jsonErr(400, 400, "模块编码已存在")
return
}
now := time.Now()
up := map[string]interface{}{
"mid": p.Mid,
"name": p.Name,
"code": p.Code,
"path": strings.TrimSpace(p.Path),
"icon": strings.TrimSpace(p.Icon),
"description": strings.TrimSpace(p.Description),
"type": p.Type,
"sort": p.Sort,
"status": p.Status,
"is_show": p.IsShow,
"update_time": now,
}
n, err := models.Orm.QueryTable(new(models.SystemModules)).
Filter("id", id).
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()
}
// Delete DELETE /platform/modules/:id(软删)
func (c *PlatformModulesController) Delete() {
if _, err := c.modulesClaims(); 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, "参数错误")
return
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemModules)).
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.Data["json"] = map[string]interface{}{"code": 200, "msg": "删除成功"}
_ = c.ServeJSON()
}
// BatchDelete POST /platform/modules/batchDelete body:{ids:[]}
func (c *PlatformModulesController) BatchDelete() {
if _, err := c.modulesClaims(); 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 struct {
IDs []uint64 `json:"ids"`
}
if err := json.Unmarshal(raw, &p); err != nil || len(p.IDs) == 0 {
c.jsonErr(400, 400, "请选择要删除的模块")
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.SystemModules)).
Filter("id__in", p.IDs).
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
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "批量删除成功"}
_ = c.ServeJSON()
}
// ChangeStatus POST /platform/modules/status body:{id,status}
// 兼容前端:这里的 status 实际用于切换 is_show(显示开关)。
func (c *PlatformModulesController) ChangeStatus() {
if _, err := c.modulesClaims(); 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 struct {
ID uint64 `json:"id"`
Status int8 `json:"status"`
}
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
c.jsonErr(400, 400, "参数错误")
return
}
if p.Status != 0 && p.Status != 1 {
p.Status = 1
}
now := time.Now()
n, err := models.Orm.QueryTable(new(models.SystemModules)).
Filter("id", p.ID).
Filter("delete_time__isnull", true).
Update(map[string]interface{}{"is_show": p.Status, "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": "success"}
_ = c.ServeJSON()
}
// GetSelectList GET /platform/modules/select/list
func (c *PlatformModulesController) GetSelectList() {
if _, err := c.modulesClaims(); 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).
OrderBy("sort", "id").
All(&rows, "ID", "Name", "Code")
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, map[string]interface{}{
"id": rows[i].ID,
"name": rows[i].Name,
"code": rows[i].Code,
})
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": list}
_ = c.ServeJSON()
}
+351
View File
@@ -0,0 +1,351 @@
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"
)
// PlatformOperationLogController 操作日志(yz_system_operation_log
type PlatformOperationLogController struct {
beego.Controller
}
func (c *PlatformOperationLogController) 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 *PlatformOperationLogController) 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 /platform/operationLogs?page=1&pageSize=20&keyword=&module=&action=&status=&startTime=&endTime=
func (c *PlatformOperationLogController) 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"))
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 /platform/operationLogs/:id
func (c *PlatformOperationLogController) Detail() {
if _, err := c.platformClaims(); 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 /platform/operationLogs/:id
func (c *PlatformOperationLogController) Delete() {
if _, err := c.platformClaims(); 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 batchDeletePayload struct {
IDs []uint64 `json:"ids"`
}
// BatchDelete POST /platform/operationLogs/batchDelete
func (c *PlatformOperationLogController) BatchDelete() {
if _, err := c.platformClaims(); 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 batchDeletePayload
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 /platform/operationLogs/statistics
// 供前端筛选项:modules/actions
func (c *PlatformOperationLogController) Statistics() {
if _, err := c.platformClaims(); 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()
}
func parseTimeFlexible(s string) (time.Time, error) {
s = strings.TrimSpace(s)
if s == "" {
return time.Time{}, fmt.Errorf("empty")
}
layouts := []string{
"2006-01-02 15:04:05",
"2006-01-02 15:04",
"2006-01-02",
time.RFC3339,
}
for _, ly := range layouts {
if t, err := time.ParseInLocation(ly, s, time.Local); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("invalid time")
}
+201
View File
@@ -0,0 +1,201 @@
package controllers
import (
"encoding/json"
"io"
"strconv"
"strings"
"server/models"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformRoleController 平台角色管理(yz_system_admin_role
type PlatformRoleController struct {
beego.Controller
}
type rolePayload struct {
Cid *uint8 `json:"cid"`
Name string `json:"name"`
Status *uint8 `json:"status"`
Rights interface{} `json:"rights"`
}
func normalizeRights(v interface{}) *string {
if v == nil {
return nil
}
switch t := v.(type) {
case string:
s := strings.TrimSpace(t)
if s == "" {
return nil
}
return &s
default:
b, err := json.Marshal(v)
if err != nil {
return nil
}
s := string(b)
return &s
}
}
// GetAllRoles 获取角色列表
// GET /platform/allRoles
func (c *PlatformRoleController) GetAllRoles() {
var rows []models.AdminRole
_, err := models.Orm.QueryTable(new(models.AdminRole)).
OrderBy("-id").
All(&rows)
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": rows}
_ = c.ServeJSON()
}
// GetRoleByID 获取角色详情
// GET /platform/roles/:id
func (c *PlatformRoleController) GetRoleByID() {
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
}
role := models.AdminRole{ID: id}
if err := models.Orm.Read(&role); err != nil {
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "角色不存在"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": role}
_ = c.ServeJSON()
}
// CreateRole 创建角色
// POST /platform/roles
func (c *PlatformRoleController) CreateRole() {
var p rolePayload
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
}
p.Name = strings.TrimSpace(p.Name)
if p.Name == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "name 不能为空"}
_ = c.ServeJSON()
return
}
status := uint8(1)
if p.Status != nil {
status = *p.Status
}
cid := uint8(1)
if p.Cid != nil {
cid = *p.Cid
}
if cid != 1 && cid != 2 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "cid 仅支持 1/2"}
_ = c.ServeJSON()
return
}
rights := normalizeRights(p.Rights)
role := &models.AdminRole{
Cid: cid,
Name: p.Name,
Status: status,
Rights: rights,
}
id, err := models.Orm.Insert(role)
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{}{"id": id}}
_ = c.ServeJSON()
}
// UpdateRole 更新角色
// PUT /platform/roles/:id
func (c *PlatformRoleController) UpdateRole() {
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
}
var p rolePayload
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
}
update := map[string]interface{}{}
if strings.TrimSpace(p.Name) != "" {
update["name"] = strings.TrimSpace(p.Name)
}
if p.Status != nil {
update["status"] = *p.Status
}
if p.Cid != nil {
if *p.Cid != 1 && *p.Cid != 2 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "cid 仅支持 1/2"}
_ = c.ServeJSON()
return
}
update["cid"] = *p.Cid
}
if p.Rights != nil {
update["rights"] = normalizeRights(p.Rights)
}
if len(update) == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无更新字段"}
_ = c.ServeJSON()
return
}
_, err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", id).Update(update)
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"}
_ = c.ServeJSON()
}
// DeleteRole 删除角色
// DELETE /platform/roles/:id
func (c *PlatformRoleController) DeleteRole() {
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
}
_, err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", id).Delete()
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"}
_ = c.ServeJSON()
}
+271
View File
@@ -0,0 +1,271 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformSiteSettingsController 租户站点设置(站点基本信息)
// 对应前端 normalSettings.vue 的:
// - GET /backend/normalInfos
// - POST /backend/saveNormalInfos
// - GET /platform/normalInfos
// - POST /platform/saveNormalInfos
type PlatformSiteSettingsController struct {
beego.Controller
}
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 *PlatformSiteSettingsController) 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 parseUint64Flexible(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 normalInfosOutput 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 *PlatformSiteSettingsController) 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 := normalInfosOutput{
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 normalInfosPayload 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 *PlatformSiteSettingsController) 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 normalInfosPayload
if uerr := json.Unmarshal(raw, &p); uerr != nil {
c.jsonErr(400, 400, "参数错误")
return
}
tid := uint64(claims.TenantId)
if tid == 0 {
tid = parseUint64Flexible(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()
}
+519
View File
@@ -0,0 +1,519 @@
package controllers
import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformSMSController 短信配置(yz_system_sms),兼容旧前端 /platform/sms/* 接口
type PlatformSMSController struct {
beego.Controller
}
func (c *PlatformSMSController) 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 *PlatformSMSController) jsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
// GetSmsInfo GET /platform/sms/info
// 返回 data[0],字段兼容 backend_url/api_key 与 backendUrl/apiKey(沿用旧前端)
func (c *PlatformSMSController) GetSmsInfo() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var row models.SystemSMS
// 优先默认通道,其次 custom
err := models.Orm.QueryTable(new(models.SystemSMS)).
Filter("is_default", 1).
Filter("status", 1).
OrderBy("-weight", "-id").
Limit(1).
One(&row)
if err != nil {
_ = models.Orm.QueryTable(new(models.SystemSMS)).
Filter("config_code", "custom").
OrderBy("-id").
Limit(1).
One(&row)
}
backendURL := strings.TrimSpace(row.ApiURL)
apiKey := strings.TrimSpace(row.ApiKey)
data := []map[string]interface{}{{
"backend_url": backendURL,
"api_key": apiKey,
"backendUrl": backendURL,
"apiKey": apiKey,
}}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "获取成功", "data": data}
_ = c.ServeJSON()
}
type smsEditPayload struct {
BackendUrl string `json:"backendUrl"`
BackendURL string `json:"backend_url"`
ApiKey string `json:"apiKey"`
APIKey string `json:"api_key"`
}
// EditSmsInfo POST /platform/sms/editinfo
// 将旧前端的 backendUrl/apiKey 落到 yz_system_sms 的 api_url/api_key(写入 config_code=custom
func (c *PlatformSMSController) EditSmsInfo() {
if _, err := c.platformClaims(); 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 smsEditPayload
if err := json.Unmarshal(raw, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
backendURL := strings.TrimSpace(p.BackendUrl)
if backendURL == "" {
backendURL = strings.TrimSpace(p.BackendURL)
}
apiKey := strings.TrimSpace(p.ApiKey)
if apiKey == "" {
apiKey = strings.TrimSpace(p.APIKey)
}
if backendURL == "" {
c.jsonErr(400, 400, "请输入短信网关地址")
return
}
if apiKey == "" {
c.jsonErr(400, 400, "请输入API KEY")
return
}
// 确保只有一个默认:先清空默认,再 upsert custom 为默认
_, _ = models.Orm.QueryTable(new(models.SystemSMS)).Update(map[string]interface{}{"is_default": 0})
var existed models.SystemSMS
e := models.Orm.QueryTable(new(models.SystemSMS)).Filter("config_code", "custom").Limit(1).One(&existed)
if e == nil && existed.ID > 0 {
_, err = models.Orm.QueryTable(new(models.SystemSMS)).Filter("id", existed.ID).Update(map[string]interface{}{
"config_name": "自定义网关",
"channel_type": 2,
"api_url": backendURL,
"api_key": apiKey,
"weight": 10,
"is_default": 1,
"status": 1,
})
if err != nil {
c.jsonErr(500, 500, "更新失败: "+err.Error())
return
}
} else {
row := &models.SystemSMS{
ConfigCode: "custom",
ConfigName: "自定义网关",
ChannelType: 2,
ApiURL: backendURL,
ApiKey: apiKey,
ApiSecret: "",
SignName: "",
TemplateID: "",
TestPhone: "",
Weight: 10,
IsDefault: 1,
Status: 1,
Remark: "",
}
if _, err := models.Orm.Insert(row); err != nil {
c.jsonErr(500, 500, "更新失败: "+err.Error())
return
}
}
updated := []map[string]interface{}{{
"backend_url": backendURL,
"api_key": apiKey,
}}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "更新成功", "data": updated}
_ = c.ServeJSON()
}
type smsTestPayload struct {
BackendUrl string `json:"backendUrl"`
BackendURL string `json:"backend_url"`
ApiKey string `json:"apiKey"`
APIKey string `json:"api_key"`
Tid *uint64 `json:"tid"`
Phone string `json:"phone"`
Content string `json:"content"`
}
// SendTestSms POST /platform/sms/sendtest
// 调用短信网关入队接口:{backendUrl}/api/v1/business/outbound-tasksheader: X-Api-Key
func (c *PlatformSMSController) SendTestSms() {
if _, err := c.platformClaims(); 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 smsTestPayload
if err := json.Unmarshal(raw, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
phone := strings.TrimSpace(p.Phone)
if phone == "" {
c.jsonErr(400, 400, "缺少测试手机号")
return
}
if !strings.HasPrefix(phone, "+") {
c.jsonErr(400, 400, "请使用国际格式手机号(以 + 开头,后为数字)")
return
}
for _, ch := range phone[1:] {
if ch < '0' || ch > '9' {
c.jsonErr(400, 400, "请使用国际格式手机号(以 + 开头,后为数字)")
return
}
}
backendURL := strings.TrimSpace(p.BackendUrl)
if backendURL == "" {
backendURL = strings.TrimSpace(p.BackendURL)
}
apiKey := strings.TrimSpace(p.ApiKey)
if apiKey == "" {
apiKey = strings.TrimSpace(p.APIKey)
}
// 兜底:body 未带时从默认配置取
if backendURL == "" || apiKey == "" {
var row models.SystemSMS
_ = models.Orm.QueryTable(new(models.SystemSMS)).
Filter("is_default", 1).
Filter("status", 1).
OrderBy("-weight", "-id").
Limit(1).
One(&row)
if backendURL == "" {
backendURL = strings.TrimSpace(row.ApiURL)
}
if apiKey == "" {
apiKey = strings.TrimSpace(row.ApiKey)
}
}
if backendURL == "" {
c.jsonErr(400, 400, "请先配置短信网关地址 backendUrl")
return
}
if apiKey == "" {
c.jsonErr(400, 400, "请先配置短信网关 API KEY")
return
}
content := strings.TrimSpace(p.Content)
code := randomDigits6()
if content == "" {
content = "短信测试验证码:" + code
}
enqueueURL := strings.TrimRight(backendURL, "/") + "/api/v1/business/outbound-tasks"
payload := map[string]interface{}{
"phone": phone,
"content": content,
}
bs, _ := json.Marshal(payload)
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("POST", enqueueURL, bytes.NewReader(bs))
if err != nil {
c.jsonErr(500, 500, "创建请求失败: "+err.Error())
return
}
req.Header.Set("X-Api-Key", apiKey)
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
c.jsonErr(500, 500, "短信网关入队失败: "+err.Error())
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
msg := strings.TrimSpace(string(body))
if msg == "" {
msg = resp.Status
}
c.jsonErr(500, 500, "短信网关入队失败: "+msg)
return
}
bodyStr := string(body)
report := strings.TrimSpace(bodyStr)
var reportPtr *string
if report != "" {
reportPtr = &bodyStr
}
// 网关 HTTP 2xx:平台侧视为「已受理并成功提交」;与前端 tasklist 中 status=3「发送成功」对齐
taskStatus := 3
// 若网关返回 JSON 且含通用状态字段,则优先映射(便于以后网关回传异步状态)
var gw map[string]interface{}
if json.Unmarshal(body, &gw) == nil {
if v, ok := gw["status"]; ok {
switch x := v.(type) {
case float64:
taskStatus = mapGatewayStatus(int(x))
case string:
if n, e := strconv.Atoi(strings.TrimSpace(x)); e == nil {
taskStatus = mapGatewayStatus(n)
}
}
}
}
// 写入本地任务表(用于前端列表/对账)
now := time.Now()
task := &models.SystemSMSTask{
Tid: p.Tid, // 测试可为空
ApiKey: apiKey,
Phone: phone,
Content: &content,
Status: taskStatus,
Code: code,
ReportRaw: reportPtr,
CreateTime: &now,
UpdateTime: &now,
}
taskID, terr := models.Orm.Insert(task)
if terr != nil {
// 入队已成功,任务写库失败也不影响短信发送,只返回提示
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "短信测试任务入队成功(任务写库失败)",
"data": map[string]interface{}{
"taskId": nil,
"code": code,
"gatewayResp": json.RawMessage(body),
},
}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "短信测试任务入队成功",
"data": map[string]interface{}{
"taskId": uint64(taskID),
"code": code,
"gatewayResp": json.RawMessage(body),
},
}
_ = c.ServeJSON()
}
// GetSmsTaskList GET /platform/sms/taskList
func (c *PlatformSMSController) GetSmsTaskList() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
statusStr := strings.TrimSpace(c.GetString("status"))
phoneKw := strings.TrimSpace(c.GetString("phone"))
tidStr := strings.TrimSpace(c.GetString("tid"))
qs := models.Orm.QueryTable(new(models.SystemSMSTask)).Filter("delete_time__isnull", true)
if statusStr != "" {
if st, err := strconv.Atoi(statusStr); err == nil {
qs = qs.Filter("status", st)
}
}
if phoneKw != "" {
qs = qs.Filter("phone__icontains", phoneKw)
}
if tidStr != "" {
if tid, err := strconv.ParseUint(tidStr, 10, 64); err == nil && tid > 0 {
qs = qs.Filter("tid", tid)
}
}
var rows []models.SystemSMSTask
_, err := qs.OrderBy("-id").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,
"api_key": rows[i].ApiKey,
"phone": rows[i].Phone,
"content": "",
"status": rows[i].Status,
"code": rows[i].Code,
"report_raw": rows[i].ReportRaw,
"create_time": "",
"update_time": "",
}
if rows[i].Tid != nil {
item["tid"] = *rows[i].Tid
}
if rows[i].Content != nil {
item["content"] = *rows[i].Content
}
if rows[i].CreateTime != nil {
item["create_time"] = rows[i].CreateTime.Format("2006-01-02 15:04:05")
}
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", "list": list}
_ = c.ServeJSON()
}
// EditSmsTask POST /platform/sms/taskEdit/:id
func (c *PlatformSMSController) EditSmsTask() {
if _, err := c.platformClaims(); 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
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
var p map[string]interface{}
_ = json.Unmarshal(raw, &p)
up := map[string]interface{}{}
if v, ok := p["status"]; ok {
switch x := v.(type) {
case float64:
up["status"] = int(x)
case string:
if n, e := strconv.Atoi(strings.TrimSpace(x)); e == nil {
up["status"] = n
}
}
}
if v, ok := p["report_raw"]; ok {
if s, ok := v.(string); ok {
up["report_raw"] = s
}
}
if v, ok := p["content"]; ok {
if s, ok := v.(string); ok {
up["content"] = s
}
}
if len(up) == 0 {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
return
}
now := time.Now()
up["update_time"] = now
n, err := models.Orm.QueryTable(new(models.SystemSMSTask)).Filter("id", id).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": "success"}
_ = c.ServeJSON()
}
func randomDigits6() string {
// 生成 6 位数字字符串
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
return "123456"
}
n := int(b[0])<<24 | int(b[1])<<16 | int(b[2])<<8 | int(b[3])
if n < 0 {
n = -n
}
code := n%900000 + 100000
return strconv.Itoa(code)
}
// mapGatewayStatus 将网关侧 status 粗略映射到前端列表:0待发送 1发送中 2失败 3成功
func mapGatewayStatus(st int) int {
switch st {
case 0:
return 0
case 1, 4, 5:
return 1
case 2, 6:
return 2
case 3:
return 3
default:
// 网关枚举未约定时:HTTP 已 2xx,按「已成功提交」显示为发送成功
return 3
}
}
+323
View File
@@ -0,0 +1,323 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"server/services"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
type PlatformSoftwareUpgradeController struct {
beego.Controller
}
func (c *PlatformSoftwareUpgradeController) 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 *PlatformSoftwareUpgradeController) 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 *PlatformSoftwareUpgradeController) ok(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
func (c *PlatformSoftwareUpgradeController) backfillDownloadURL(productID uint64) {
var row models.SystemSoftwareUpgrade
err := models.Orm.QueryTable(new(models.SystemSoftwareUpgrade)).
Filter("id", productID).
Filter("delete_time__isnull", true).
One(&row)
if err != nil {
return
}
if row.FileID == nil || *row.FileID == 0 {
return
}
if row.DownloadURL != nil && strings.TrimSpace(*row.DownloadURL) != "" {
return
}
scheme, host := services.PublicRequestBaseURL(&c.Controller)
u := services.ResolveSoftwareDownloadURL(scheme, host, nil, row.FileID)
if u == "" {
return
}
_, _ = models.Orm.QueryTable(new(models.SystemSoftwareUpgrade)).
Filter("id", productID).
Update(map[string]interface{}{"download_url": u})
}
func (c *PlatformSoftwareUpgradeController) rowToMap(row *models.SystemSoftwareUpgrade) map[string]interface{} {
scheme, host := services.PublicRequestBaseURL(&c.Controller)
resolved := services.ResolveSoftwareDownloadURL(scheme, host, row.DownloadURL, row.FileID)
return map[string]interface{}{
"id": row.ID,
"name": row.Name,
"code": row.Code,
"latestVersion": row.LatestVersion,
"fileId": row.FileID,
"downloadUrl": row.DownloadURL,
"resolvedDownloadUrl": resolved,
"forceUpdate": row.ForceUpdate,
"releaseNotes": row.ReleaseNotes,
"status": row.Status,
"sort": row.Sort,
"createTime": row.CreateTime,
"updateTime": row.UpdateTime,
}
}
// List GET /platform/softwareupgrade/list
func (c *PlatformSoftwareUpgradeController) 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"))
qs := models.Orm.QueryTable(new(models.SystemSoftwareUpgrade)).Filter("delete_time__isnull", true)
if keyword != "" {
cond := orm.NewCondition().Or("name__icontains", keyword).Or("code__icontains", keyword)
qs = qs.SetCond(cond)
}
total, _ := qs.Count()
var rows []models.SystemSoftwareUpgrade
_, err := qs.OrderBy("sort", "-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/softwareupgrade/:id
func (c *PlatformSoftwareUpgradeController) 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.SystemSoftwareUpgrade
err = models.Orm.QueryTable(new(models.SystemSoftwareUpgrade)).
Filter("id", id).
Filter("delete_time__isnull", true).
One(&row)
if err != nil {
c.jsonErr(404, 404, "记录不存在")
return
}
c.ok(c.rowToMap(&row))
}
type softwareUpgradePayload struct {
Name *string `json:"name"`
Code *string `json:"code"`
LatestVersion *string `json:"latestVersion"`
FileID *uint64 `json:"fileId"`
DownloadURL *string `json:"downloadUrl"`
ForceUpdate *int8 `json:"forceUpdate"`
ReleaseNotes *string `json:"releaseNotes"`
Status *int8 `json:"status"`
Sort *int `json:"sort"`
}
// Create POST /platform/softwareupgrade
func (c *PlatformSoftwareUpgradeController) Create() {
if _, err := c.platformClaims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
body, _ := io.ReadAll(c.Ctx.Request.Body)
var p softwareUpgradePayload
if err := json.Unmarshal(body, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
if p.Name == nil || strings.TrimSpace(*p.Name) == "" || p.Code == nil || strings.TrimSpace(*p.Code) == "" {
c.jsonErr(400, 400, "名称与产品标识 code 不能为空")
return
}
v := "0.0.0"
if p.LatestVersion != nil && strings.TrimSpace(*p.LatestVersion) != "" {
v = strings.TrimSpace(*p.LatestVersion)
}
row := models.SystemSoftwareUpgrade{
Name: strings.TrimSpace(*p.Name),
Code: strings.TrimSpace(*p.Code),
LatestVersion: v,
DownloadURL: p.DownloadURL,
ForceUpdate: 0,
Status: 1,
Sort: 0,
}
if p.ForceUpdate != nil {
row.ForceUpdate = *p.ForceUpdate
}
if p.ReleaseNotes != nil {
row.ReleaseNotes = p.ReleaseNotes
}
if p.Status != nil {
row.Status = *p.Status
}
if p.Sort != nil {
row.Sort = *p.Sort
}
if p.FileID != nil && *p.FileID > 0 {
row.FileID = p.FileID
}
id, err := models.Orm.Insert(&row)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
c.jsonErr(400, 400, "产品标识 code 已存在")
return
}
c.jsonErr(500, 500, "创建失败: "+err.Error())
return
}
c.backfillDownloadURL(uint64(id))
c.ok(map[string]interface{}{"id": id})
}
// Update POST /platform/softwareupgrade/:id
func (c *PlatformSoftwareUpgradeController) Update() {
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
}
body, _ := io.ReadAll(c.Ctx.Request.Body)
var p softwareUpgradePayload
if err := json.Unmarshal(body, &p); err != nil {
c.jsonErr(400, 400, "参数错误")
return
}
up := map[string]interface{}{}
if p.Name != nil {
up["name"] = strings.TrimSpace(*p.Name)
}
if p.Code != nil {
up["code"] = strings.TrimSpace(*p.Code)
}
if p.LatestVersion != nil {
up["latest_version"] = strings.TrimSpace(*p.LatestVersion)
}
if p.FileID != nil {
if *p.FileID == 0 {
up["file_id"] = nil
} else {
up["file_id"] = *p.FileID
}
}
if p.DownloadURL != nil {
up["download_url"] = strings.TrimSpace(*p.DownloadURL)
}
if p.ForceUpdate != nil {
up["force_update"] = *p.ForceUpdate
}
if p.ReleaseNotes != nil {
up["release_notes"] = *p.ReleaseNotes
}
if p.Status != nil {
up["status"] = *p.Status
}
if p.Sort != nil {
up["sort"] = *p.Sort
}
if len(up) == 0 {
c.jsonErr(400, 400, "无更新字段")
return
}
n, err := models.Orm.QueryTable(new(models.SystemSoftwareUpgrade)).
Filter("id", id).
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.backfillDownloadURL(id)
c.ok(nil)
}
// Delete DELETE /platform/softwareupgrade/:id
func (c *PlatformSoftwareUpgradeController) 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.SystemSoftwareUpgrade)).
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)
}
+343
View File
@@ -0,0 +1,343 @@
package controllers
import (
"encoding/json"
"io"
"strconv"
"strings"
"time"
"server/models"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformTenantController 平台端租户管ç?
type PlatformTenantController struct {
beego.Controller
}
type tenantDTO struct {
ID uint64 `json:"id"`
TenantCode string `json:"tenant_code"`
TenantName string `json:"tenant_name"`
ContactPerson string `json:"contact_person"`
ContactPhone string `json:"contact_phone"`
ContactEmail string `json:"contact_email"`
Address string `json:"address"`
Worktime string `json:"worktime"`
Status int8 `json:"status"`
Remark string `json:"remark"`
CreateTime *time.Time `json:"create_time,omitempty"`
UpdateTime *time.Time `json:"update_time,omitempty"`
DeleteTime *time.Time `json:"delete_time,omitempty"`
}
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: stringValue(t.ContactPerson),
ContactPhone: stringValue(t.ContactPhone),
ContactEmail: stringValue(t.ContactEmail),
Address: stringValue(t.Address),
Worktime: stringValue(t.Worktime),
Status: t.Status,
Remark: stringValue(t.Remark),
CreateTime: &ct,
UpdateTime: &ut,
DeleteTime: t.DeleteTime,
}
}
// 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)
pageSize, _ := c.GetInt("pageSize", 10)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
tenantName := strings.TrimSpace(c.GetString("tenant_name"))
tenantCode := strings.TrimSpace(c.GetString("tenant_code"))
contactPerson := strings.TrimSpace(c.GetString("contact_person"))
contactPhone := strings.TrimSpace(c.GetString("contact_phone"))
qs := models.Orm.QueryTable(new(models.SystemTenant))
if tenantName != "" {
qs = qs.Filter("tenant_name__icontains", tenantName)
}
if tenantCode != "" {
qs = qs.Filter("tenant_code__icontains", tenantCode)
}
if contactPerson != "" {
qs = qs.Filter("contact_person__icontains", contactPerson)
}
if contactPhone != "" {
qs = qs.Filter("contact_phone__icontains", contactPhone)
}
total, err := qs.Count()
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取租户失败: " + err.Error()}
_ = c.ServeJSON()
return
}
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.ServeJSON()
return
}
list := make([]tenantDTO, 0, len(rows))
for _, t := range rows {
list = append(list, toTenantDTO(t))
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"list": list,
"total": total,
},
}
_ = c.ServeJSON()
}
// 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.ServeJSON()
return
}
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()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": toTenantDTO(t),
}
_ = c.ServeJSON()
}
type tenantPayload struct {
TenantCode string `json:"tenant_code"`
TenantName string `json:"tenant_name"`
ContactPerson string `json:"contact_person"`
ContactPhone string `json:"contact_phone"`
ContactEmail string `json:"contact_email"`
Address string `json:"address"`
Worktime string `json:"worktime"`
Status *int8 `json:"status"`
Remark string `json:"remark"`
}
func (c *PlatformTenantController) parseTenantPayload() (tenantPayload, error) {
// 优先从表单读取(createTenant 使用 multipart/form-dataďź?
p := tenantPayload{
TenantCode: strings.TrimSpace(c.GetString("tenant_code")),
TenantName: strings.TrimSpace(c.GetString("tenant_name")),
ContactPerson: strings.TrimSpace(c.GetString("contact_person")),
ContactPhone: strings.TrimSpace(c.GetString("contact_phone")),
ContactEmail: strings.TrimSpace(c.GetString("contact_email")),
Address: strings.TrimSpace(c.GetString("address")),
Worktime: strings.TrimSpace(c.GetString("worktime")),
Remark: strings.TrimSpace(c.GetString("remark")),
}
if s := strings.TrimSpace(c.GetString("status")); s != "" {
if v, err := strconv.ParseInt(s, 10, 8); err == nil {
tmp := int8(v)
p.Status = &tmp
}
}
// 如果关键字段为空,尝试从 JSON body 解析(editTenant 靘莤 JSONďź?
if p.TenantName == "" && p.TenantCode == "" {
raw, _ := io.ReadAll(c.Ctx.Request.Body)
if len(raw) > 0 {
_ = json.Unmarshal(raw, &p)
}
}
return p, nil
}
// 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.ServeJSON()
return
}
if strings.TrimSpace(p.TenantCode) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户编码不能为空"}
_ = c.ServeJSON()
return
}
// 校验编码唯一
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.ServeJSON()
return
}
if cnt > 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户编码已存在"}
_ = c.ServeJSON()
return
}
status := int8(1)
if p.Status != nil {
status = *p.Status
}
t := models.SystemTenant{
TenantCode: p.TenantCode,
TenantName: p.TenantName,
ContactPerson: stringPtr(p.ContactPerson),
ContactPhone: stringPtr(p.ContactPhone),
ContactEmail: stringPtr(p.ContactEmail),
Address: stringPtr(p.Address),
Worktime: stringPtr(p.Worktime),
Status: status,
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.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"id": id},
}
_ = c.ServeJSON()
}
// 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.ServeJSON()
return
}
p, _ := c.parseTenantPayload()
if strings.TrimSpace(p.TenantName) == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "租户名称不能为空"}
_ = c.ServeJSON()
return
}
update := map[string]interface{}{
"tenant_name": p.TenantName,
"contact_person": p.ContactPerson,
"contact_phone": p.ContactPhone,
"contact_email": p.ContactEmail,
"address": p.Address,
"worktime": p.Worktime,
"remark": p.Remark,
}
if p.Status != nil {
update["status"] = *p.Status
}
_, 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.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// 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.ServeJSON()
return
}
_, 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.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// FindTenantCode 校验租户编码是否重复
// GET /platform/tenant/findTenantCode?tenant_code=xxxxxx
// 返回 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.ServeJSON()
return
}
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.ServeJSON()
return
}
if cnt > 0 {
c.Data["json"] = map[string]interface{}{"code": 409, "msg": "租户编码已存在"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok"}
_ = c.ServeJSON()
}
+332
View File
@@ -0,0 +1,332 @@
package controllers
import (
"encoding/json"
"errors"
"io"
"math/rand"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/passwordutil"
"server/services"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformTenantUserController 平台租户用户绑定管理
type PlatformTenantUserController struct {
beego.Controller
}
type tenantUserPayload 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"`
Password *string `json:"password"`
IsDefault *int8 `json:"is_default"`
Status *int8 `json:"status"`
Remark *string `json:"remark"`
}
// 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.SystemTenantUser))
var cond *orm.Condition
needCond := false
if tid > 0 {
if cond == nil {
cond = orm.NewCondition()
}
cond = cond.And("tid", tid)
needCond = true
}
if uid > 0 {
if cond == nil {
cond = orm.NewCondition()
}
cond = cond.And("uid", uid)
needCond = true
}
if keyword != "" {
kwCond := orm.NewCondition()
kwCond = kwCond.Or("name__icontains", keyword).
Or("phone__icontains", keyword).
Or("email__icontains", keyword).
Or("account__icontains", keyword)
if cond == nil {
cond = kwCond
} else {
cond = cond.AndCond(kwCond)
}
needCond = true
}
if needCond {
qs = qs.SetCond(cond)
}
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()}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{
"list": rows,
"total": len(rows),
},
}
_ = c.ServeJSON()
}
// GetTenantUsersByTid 兼容旧路由,根据租户 ID 获取租户用户列表
// GET /platform/getTenantUsers/:tid
func (c *PlatformTenantUserController) GetTenantUsersByTid() {
tidStr := c.Ctx.Input.Param(":tid")
tid, _ := strconv.ParseUint(tidStr, 10, 64)
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).
OrderBy("-is_default", "-id").
All(&rows)
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": "success",
"data": map[string]interface{}{"list": rows, "total": len(rows)},
}
_ = c.ServeJSON()
}
// GetTenantUserDetail 获取绑定详情
// GET /platform/tenantUser/detail/:id
func (c *PlatformTenantUserController) GetTenantUserDetail() {
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.ServeJSON()
return
}
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()
return
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": row}
_ = c.ServeJSON()
}
// CreateTenantUser 创建租户用户绑定(写入 yz_system_tenant_useruid 为空时自动生成)
// POST /platform/tenantUser/create
func (c *PlatformTenantUserController) CreateTenantUser() {
p, ok := c.parsePayload()
if !ok {
return
}
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
}
hashed, err := passwordutil.Hash(*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)
status := int8(1)
if p.IsDefault != nil {
isDefault = *p.IsDefault
}
if p.Status != nil {
status = *p.Status
}
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()
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()
}
// EditTenantUser 编辑绑定
// POST /platform/tenantUser/edit/:id
func (c *PlatformTenantUserController) EditTenantUser() {
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.ServeJSON()
return
}
p, ok := c.parsePayload()
if !ok {
return
}
update := map[string]interface{}{}
if p.Tid > 0 {
update["tid"] = p.Tid
}
if p.Uid > 0 {
update["uid"] = p.Uid
}
if p.Account != nil {
update["account"] = p.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.Password != nil {
hashed, err := passwordutil.Hash(*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", 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 && p.Uid > 0 && p.Tid > 0 {
_ = services.SetDefaultTenant(p.Uid, p.Tid)
}
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
_ = c.ServeJSON()
}
// DeleteTenantUser 删除绑定
// DELETE /platform/tenantUser/delete/:id
func (c *PlatformTenantUserController) DeleteTenantUser() {
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.ServeJSON()
return
}
if err := services.UnbindTenantUser(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()
}
func (c *PlatformTenantUserController) parsePayload() (tenantUserPayload, bool) {
var p tenantUserPayload
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 tenantUserPayload{}, false
}
return p, true
}
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.SystemTenantUser)).
Filter("tid", tid).
Filter("uid", uid).
Count()
if err != nil {
return 0, err
}
if cnt == 0 {
return uid, nil
}
}
return 0, errors.New("uid collision")
}
+107
View File
@@ -0,0 +1,107 @@
package controllers
import (
"encoding/json"
"io"
"math/rand"
"strings"
"time"
"server/pkg/passwordutil"
"server/services"
beego "github.com/beego/beego/v2/server/web"
)
// PlatformUserController 平台端用户相关(简化:当前用户信息落在 yz_system_tenant_user
type PlatformUserController struct {
beego.Controller
}
type addUserPayload struct {
Tid uint64 `json:"tid"`
Account string `json:"account"`
Password string `json:"password"`
Name string `json:"name"`
Phone string `json:"phone"`
Email string `json:"email"`
Status *int8 `json:"status"`
Remark *string `json:"remark"`
}
// AddUser 添加用户(绑定到租户)
// POST /platform/addUser
func (c *PlatformUserController) AddUser() {
var p addUserPayload
// 兼容 JSON body
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
}
p.Account = strings.TrimSpace(p.Account)
p.Password = strings.TrimSpace(p.Password)
p.Name = strings.TrimSpace(p.Name)
p.Phone = strings.TrimSpace(p.Phone)
p.Email = strings.TrimSpace(p.Email)
if p.Tid == 0 {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tid 不能为空"}
_ = c.ServeJSON()
return
}
if p.Account == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "account 不能为空"}
_ = c.ServeJSON()
return
}
if p.Password == "" {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "password 不能为空"}
_ = c.ServeJSON()
return
}
hashed, err := passwordutil.Hash(p.Password)
if err != nil {
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
_ = c.ServeJSON()
return
}
status := int8(1)
if p.Status != nil {
status = *p.Status
}
// 生成 uid8位数字即可(10000000~99999999
rand.Seed(time.Now().UnixNano())
var uid uint64
for i := 0; i < 5; i++ {
uid = uint64(10000000 + rand.Intn(90000000))
// 尝试写入(若冲突由唯一索引兜底,外层再重试)
account := &p.Account
name := &p.Name
phone := &p.Phone
email := &p.Email
hashedPwd := hashed
password := &hashedPwd
_, 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,
"msg": "success",
"data": map[string]interface{}{"tid": p.Tid, "uid": uid},
}
_ = c.ServeJSON()
return
}
// 轻量重试
time.Sleep(5 * time.Millisecond)
}
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "添加失败,请重试"}
_ = c.ServeJSON()
}
+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()
}