backend增加租户简称登录

This commit is contained in:
2026-07-17 11:00:33 +08:00
parent dcf06db11b
commit 2232ef6f3c
14 changed files with 916 additions and 746 deletions
+6 -1
View File
@@ -78,7 +78,7 @@ func (c *BackendNotebookController) List() {
total, _ := qs.Count()
var list []models.BackendNotebook
_, err = qs.OrderBy("-update_time", "-create_time").
_, err = qs.OrderBy("-pinned", "-id").
Limit(pageSize).Offset((page - 1) * pageSize).
All(&list)
if err != nil && err != orm.ErrNoRows {
@@ -137,6 +137,7 @@ func (c *BackendNotebookController) Create() {
var payload struct {
Title string `json:"title"`
Content string `json:"content"`
Pinned int8 `json:"pinned"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
c.nbJsonErr(400, 400, "参数错误")
@@ -153,6 +154,7 @@ func (c *BackendNotebookController) Create() {
Tid: claims.TenantId,
Title: payload.Title,
Content: payload.Content,
Pinned: payload.Pinned,
UserID: &userID,
UserName: &claims.Username,
IsDeleted: 0,
@@ -190,6 +192,7 @@ func (c *BackendNotebookController) Update() {
var payload struct {
Title string `json:"title"`
Content string `json:"content"`
Pinned int8 `json:"pinned"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
c.nbJsonErr(400, 400, "参数错误")
@@ -219,6 +222,7 @@ func (c *BackendNotebookController) Update() {
Update(map[string]interface{}{
"title": payload.Title,
"content": payload.Content,
"pinned": payload.Pinned,
"update_time": now,
})
if err != nil {
@@ -228,6 +232,7 @@ func (c *BackendNotebookController) Update() {
note.Title = payload.Title
note.Content = payload.Content
note.Pinned = payload.Pinned
note.UpdateTime = &now
c.nbOk(note)
}
+317 -312
View File
@@ -1,312 +1,317 @@
package controllers
import (
"encoding/json"
"io"
"server/models"
"server/pkg/jwtutil"
"strconv"
"strings"
"time"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
type PlatformNotebookController struct {
beego.Controller
}
// requireAuth 验证平台用户权限
func requireNotebookAuth(c *beego.Controller) (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, orm.ErrNoRows
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, orm.ErrNoRows
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, err
}
if claims.UserType != "platform" {
return nil, orm.ErrNoRows
}
return claims, nil
}
// jsonResponse 统一JSON响应
func jsonResponse(c *beego.Controller, httpStatus, code int, msg string, data interface{}) {
c.Ctx.Output.SetStatus(httpStatus)
resp := map[string]interface{}{
"code": code,
"msg": msg,
}
if data != nil {
resp["data"] = data
}
c.Data["json"] = resp
_ = c.ServeJSON()
}
// List 获取笔记列表
// GET /platform/notebook/list
func (c *PlatformNotebookController) List() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
keyword := strings.TrimSpace(c.GetString("keyword"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
qs := models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("is_deleted", 0).
Filter("user_id", claims.UserID)
if keyword != "" {
qs = qs.Filter("title__icontains", keyword)
}
total, err := qs.Count()
if err != nil {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
return
}
var list []models.PlatformNotebook
_, err = qs.OrderBy("-update_time", "-create_time").
Limit(pageSize).
Offset((page - 1) * pageSize).
All(&list)
if err != nil && err != orm.ErrNoRows {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
return
}
if list == nil {
list = []models.PlatformNotebook{}
}
jsonResponse(&c.Controller, 200, 200, "success", map[string]interface{}{
"list": list,
"total": total,
})
}
// Detail 获取笔记详情
// GET /platform/notebook/detail/:id
func (c *PlatformNotebookController) Detail() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
return
}
var note models.PlatformNotebook
err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Filter("is_deleted", 0).
Filter("user_id", claims.UserID).
One(&note)
if err != nil {
if err == orm.ErrNoRows {
jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
} else {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
}
return
}
jsonResponse(&c.Controller, 200, 200, "success", note)
}
// Create 创建笔记
// POST /platform/notebook/create
func (c *PlatformNotebookController) Create() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return
}
var payload struct {
Title string `json:"title"`
Content string `json:"content"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return
}
payload.Title = strings.TrimSpace(payload.Title)
if payload.Title == "" {
payload.Title = "无标题"
}
userID := uint64(claims.UserID)
note := &models.PlatformNotebook{
Title: payload.Title,
Content: payload.Content,
UserID: &userID,
UserName: &claims.Username,
IsDeleted: 0,
}
id, err := models.Orm.Insert(note)
if err != nil {
jsonResponse(&c.Controller, 500, 500, "创建失败", nil)
return
}
note.ID = uint64(id)
jsonResponse(&c.Controller, 200, 200, "创建成功", note)
}
// Update 更新笔记
// POST /platform/notebook/update/:id
func (c *PlatformNotebookController) Update() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return
}
var payload struct {
Title string `json:"title"`
Content string `json:"content"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return
}
payload.Title = strings.TrimSpace(payload.Title)
if payload.Title == "" {
payload.Title = "无标题"
}
// 验证笔记是否存在且属于当前用户
var note models.PlatformNotebook
err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Filter("is_deleted", 0).
Filter("user_id", claims.UserID).
One(&note)
if err != nil {
if err == orm.ErrNoRows {
jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
} else {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
}
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Update(map[string]interface{}{
"title": payload.Title,
"content": payload.Content,
"update_time": now,
})
if err != nil {
jsonResponse(&c.Controller, 500, 500, "更新失败", nil)
return
}
note.Title = payload.Title
note.Content = payload.Content
note.UpdateTime = &now
jsonResponse(&c.Controller, 200, 200, "更新成功", note)
}
// Delete 删除笔记(软删除)
// DELETE /platform/notebook/delete/:id
func (c *PlatformNotebookController) Delete() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
return
}
// 验证笔记是否存在且属于当前用户
var note models.PlatformNotebook
err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Filter("is_deleted", 0).
Filter("user_id", claims.UserID).
One(&note)
if err != nil {
if err == orm.ErrNoRows {
jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
} else {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
}
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Update(map[string]interface{}{
"is_deleted": 1,
"delete_time": now,
})
if err != nil {
jsonResponse(&c.Controller, 500, 500, "删除失败", nil)
return
}
jsonResponse(&c.Controller, 200, 200, "删除成功", nil)
}
package controllers
import (
"encoding/json"
"io"
"server/models"
"server/pkg/jwtutil"
"strconv"
"strings"
"time"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
type PlatformNotebookController struct {
beego.Controller
}
// requireAuth 验证平台用户权限
func requireNotebookAuth(c *beego.Controller) (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, orm.ErrNoRows
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, orm.ErrNoRows
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, err
}
if claims.UserType != "platform" {
return nil, orm.ErrNoRows
}
return claims, nil
}
// jsonResponse 统一JSON响应
func jsonResponse(c *beego.Controller, httpStatus, code int, msg string, data interface{}) {
c.Ctx.Output.SetStatus(httpStatus)
resp := map[string]interface{}{
"code": code,
"msg": msg,
}
if data != nil {
resp["data"] = data
}
c.Data["json"] = resp
_ = c.ServeJSON()
}
// List 获取笔记列表
// GET /platform/notebook/list
func (c *PlatformNotebookController) List() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
keyword := strings.TrimSpace(c.GetString("keyword"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
qs := models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("is_deleted", 0).
Filter("user_id", claims.UserID)
if keyword != "" {
qs = qs.Filter("title__icontains", keyword)
}
total, err := qs.Count()
if err != nil {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
return
}
var list []models.PlatformNotebook
_, err = qs.OrderBy("-pinned", "-id").
Limit(pageSize).
Offset((page - 1) * pageSize).
All(&list)
if err != nil && err != orm.ErrNoRows {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
return
}
if list == nil {
list = []models.PlatformNotebook{}
}
jsonResponse(&c.Controller, 200, 200, "success", map[string]interface{}{
"list": list,
"total": total,
})
}
// Detail 获取笔记详情
// GET /platform/notebook/detail/:id
func (c *PlatformNotebookController) Detail() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
return
}
var note models.PlatformNotebook
err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Filter("is_deleted", 0).
Filter("user_id", claims.UserID).
One(&note)
if err != nil {
if err == orm.ErrNoRows {
jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
} else {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
}
return
}
jsonResponse(&c.Controller, 200, 200, "success", note)
}
// Create 创建笔记
// POST /platform/notebook/create
func (c *PlatformNotebookController) Create() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return
}
var payload struct {
Title string `json:"title"`
Content string `json:"content"`
Pinned int8 `json:"pinned"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return
}
payload.Title = strings.TrimSpace(payload.Title)
if payload.Title == "" {
payload.Title = "无标题"
}
userID := uint64(claims.UserID)
note := &models.PlatformNotebook{
Title: payload.Title,
Content: payload.Content,
Pinned: payload.Pinned,
UserID: &userID,
UserName: &claims.Username,
IsDeleted: 0,
}
id, err := models.Orm.Insert(note)
if err != nil {
jsonResponse(&c.Controller, 500, 500, "创建失败", nil)
return
}
note.ID = uint64(id)
jsonResponse(&c.Controller, 200, 200, "创建成功", note)
}
// Update 更新笔记
// POST /platform/notebook/update/:id
func (c *PlatformNotebookController) Update() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
return
}
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return
}
var payload struct {
Title string `json:"title"`
Content string `json:"content"`
Pinned int8 `json:"pinned"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
jsonResponse(&c.Controller, 400, 400, "参数错误", nil)
return
}
payload.Title = strings.TrimSpace(payload.Title)
if payload.Title == "" {
payload.Title = "无标题"
}
// 验证笔记是否存在且属于当前用户
var note models.PlatformNotebook
err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Filter("is_deleted", 0).
Filter("user_id", claims.UserID).
One(&note)
if err != nil {
if err == orm.ErrNoRows {
jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
} else {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
}
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Update(map[string]interface{}{
"title": payload.Title,
"content": payload.Content,
"pinned": payload.Pinned,
"update_time": now,
})
if err != nil {
jsonResponse(&c.Controller, 500, 500, "更新失败", nil)
return
}
note.Title = payload.Title
note.Content = payload.Content
note.Pinned = payload.Pinned
note.UpdateTime = &now
jsonResponse(&c.Controller, 200, 200, "更新成功", note)
}
// Delete 删除笔记(软删除)
// DELETE /platform/notebook/delete/:id
func (c *PlatformNotebookController) Delete() {
claims, err := requireNotebookAuth(&c.Controller)
if err != nil {
jsonResponse(&c.Controller, 401, 401, "未登录或无权限", nil)
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
jsonResponse(&c.Controller, 400, 400, "无效ID", nil)
return
}
// 验证笔记是否存在且属于当前用户
var note models.PlatformNotebook
err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Filter("is_deleted", 0).
Filter("user_id", claims.UserID).
One(&note)
if err != nil {
if err == orm.ErrNoRows {
jsonResponse(&c.Controller, 404, 404, "笔记不存在", nil)
} else {
jsonResponse(&c.Controller, 500, 500, "查询失败", nil)
}
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.PlatformNotebook)).
Filter("id", id).
Update(map[string]interface{}{
"is_deleted": 1,
"delete_time": now,
})
if err != nil {
jsonResponse(&c.Controller, 500, 500, "删除失败", nil)
return
}
jsonResponse(&c.Controller, 200, 200, "删除成功", nil)
}
+21 -20
View File
@@ -1,20 +1,21 @@
package models
import "time"
// PlatformNotebook 平台记事本表: yz_platform_notebook
type PlatformNotebook struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Title string `orm:"column(title);size(255)" json:"title"`
Content string `orm:"column(content);type(longtext);null" json:"content"`
UserID *uint64 `orm:"column(user_id);null" json:"user_id"`
UserName *string `orm:"column(user_name);size(100);null" json:"user_name"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformNotebook) TableName() string {
return "yz_platform_notebook"
}
package models
import "time"
// PlatformNotebook 平台记事本表: yz_platform_notebook
type PlatformNotebook struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Title string `orm:"column(title);size(255)" json:"title"`
Content string `orm:"column(content);type(longtext);null" json:"content"`
Pinned int8 `orm:"column(pinned);default(0)" json:"pinned"`
UserID *uint64 `orm:"column(user_id);null" json:"user_id"`
UserName *string `orm:"column(user_name);size(100);null" json:"user_name"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *PlatformNotebook) TableName() string {
return "yz_platform_notebook"
}
+24 -23
View File
@@ -1,23 +1,24 @@
package models
import "time"
type SystemTenant struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
TenantCode string `orm:"column(tenant_code);size(32)" json:"tenant_code"`
TenantName string `orm:"column(tenant_name);size(128)" json:"tenant_name"`
ContactPerson *string `orm:"column(contact_person);size(64);null" json:"contact_person"`
ContactPhone *string `orm:"column(contact_phone);size(20);null" json:"contact_phone"`
ContactEmail *string `orm:"column(contact_email);size(128);null" json:"contact_email"`
Address *string `orm:"column(address);size(255);null" json:"address"`
Worktime *string `orm:"column(worktime);size(255);null" json:"worktime"`
Status int8 `orm:"column(status);default(1)" json:"status"`
Remark *string `orm:"column(remark);size(512);null" json:"remark"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *SystemTenant) TableName() string {
return "yz_system_tenant"
}
package models
import "time"
type SystemTenant struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
TenantCode string `orm:"column(tenant_code);size(32)" json:"tenant_code"`
TenantName string `orm:"column(tenant_name);size(128)" json:"tenant_name"`
TenantShortName *string `orm:"column(tenant_short_name);size(128);null" json:"tenant_short_name"`
ContactPerson *string `orm:"column(contact_person);size(64);null" json:"contact_person"`
ContactPhone *string `orm:"column(contact_phone);size(20);null" json:"contact_phone"`
ContactEmail *string `orm:"column(contact_email);size(128);null" json:"contact_email"`
Address *string `orm:"column(address);size(255);null" json:"address"`
Worktime *string `orm:"column(worktime);size(255);null" json:"worktime"`
Status int8 `orm:"column(status);default(1)" json:"status"`
Remark *string `orm:"column(remark);size(512);null" json:"remark"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *SystemTenant) TableName() string {
return "yz_system_tenant"
}
+232 -232
View File
@@ -1,232 +1,232 @@
package services
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"strings"
"sync"
"time"
"server/models"
)
type loginCodeItem struct {
Code string
Channel string
ExpiredAt time.Time
}
var loginCodeStore sync.Map
func codeKey(account, channel string) string {
return strings.ToLower(strings.TrimSpace(account)) + "|" + strings.TrimSpace(channel)
}
func SendPlatformLoginCode(account, channel string) error {
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
if account == "" {
return errors.New("账号不能为空")
}
if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码")
}
var u models.AdminUser
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("account", account).One(&u); err != nil {
return errors.New("用户不存在")
}
if u.Status == 0 {
return errors.New("账号已禁用")
}
if channel == "sms" && (u.Phone == nil || strings.TrimSpace(*u.Phone) == "") {
return errors.New("该账号未绑定手机号")
}
if channel == "email" && (u.Email == nil || strings.TrimSpace(*u.Email) == "") {
return errors.New("该账号未绑定邮箱")
}
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
loginCodeStore.Store(codeKey(account, channel), loginCodeItem{
Code: code,
Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute),
})
return nil
}
func VerifyPlatformLoginCode(account, channel, code string) error {
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
code = strings.TrimSpace(code)
if account == "" || code == "" {
return errors.New("验证码不能为空")
}
val, ok := loginCodeStore.Load(codeKey(account, channel))
if !ok {
return errors.New("验证码不存在或已失效")
}
item, ok := val.(loginCodeItem)
if !ok {
return errors.New("验证码状态异常")
}
if time.Now().After(item.ExpiredAt) {
loginCodeStore.Delete(codeKey(account, channel))
return errors.New("验证码已过期")
}
if item.Code != code {
return errors.New("验证码错误")
}
loginCodeStore.Delete(codeKey(account, channel))
return nil
}
func SendBackendLoginCode(tenantName, account, channel string) error {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
if tenantName == "" || account == "" {
return errors.New("租户名称和账号不能为空")
}
if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码")
}
var tenant models.SystemTenant
if err := models.Orm.QueryTable(new(models.SystemTenant)).Filter("tenant_name", tenantName).One(&tenant); err != nil {
return errors.New("租户不存在")
}
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
switch channel {
case "sms":
phone := account
var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("phone", phone).
One(&user); err != nil {
return errors.New("该手机号非当前企业绑定号码,请重试")
}
if user.Status == 0 {
return errors.New("账号已禁用")
}
if user.Phone == nil || strings.TrimSpace(*user.Phone) == "" {
return errors.New("该手机号非当前企业绑定号码,请重试")
}
content := "短信验证码:" + code
if err := enqueueSMSTaskForLogin(tenant.ID, phone, content, code); err != nil {
return errors.New("短信发送失败,请重试")
}
case "email":
email := account
var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("email", email).
One(&user); err != nil {
return errors.New("该账号未绑定邮箱")
}
if user.Status == 0 {
return errors.New("账号已禁用")
}
if user.Email == nil || strings.TrimSpace(*user.Email) == "" {
return errors.New("该账号未绑定邮箱")
}
}
loginCodeStore.Store(codeKey(tenantName+"#"+account, channel), loginCodeItem{
Code: code,
Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute),
})
return nil
}
func VerifyBackendLoginCode(tenantName, account, channel, code string) error {
return VerifyPlatformLoginCode(tenantName+"#"+account, channel, code)
}
func getDefaultSystemSMSConfig() (backendURL string, apiKey string, err error) {
backendURL = models.GetPlatformSettingValue("sms_custom_url", "")
apiKey = models.GetPlatformSettingValue("sms_custom_key", "")
if backendURL == "" || apiKey == "" {
return "", "", fmt.Errorf("短信网关未配置")
}
return backendURL, apiKey, nil
}
// enqueueSMSTaskForLogin 入队短信任务到网关,并写入 yz_system_sms_tasks
func enqueueSMSTaskForLogin(tid uint64, phone, content, code string) error {
backendURL, apiKey, err := getDefaultSystemSMSConfig()
if err != nil {
return err
}
if backendURL == "" || apiKey == "" {
return errors.New("短信网关未配置")
}
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 {
return err
}
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 {
return err
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
bodyStr := strings.TrimSpace(string(bodyBytes))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("gateway http status: %d, body: %s", resp.StatusCode, bodyStr)
}
now := time.Now()
tidCopy := tid
contentPtr := content
var reportPtr *string
if bodyStr != "" {
reportPtr = &bodyStr
}
task := &models.SystemSMSTask{
Tid: &tidCopy,
ApiKey: apiKey,
Phone: phone,
Content: &contentPtr,
Status: 3,
Code: code,
ReportRaw: reportPtr,
CreateTime: &now,
UpdateTime: &now,
}
_, insertErr := models.Orm.Insert(task)
if insertErr != nil {
return nil
}
return nil
}
package services
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"strings"
"sync"
"time"
"server/models"
)
type loginCodeItem struct {
Code string
Channel string
ExpiredAt time.Time
}
var loginCodeStore sync.Map
func codeKey(account, channel string) string {
return strings.ToLower(strings.TrimSpace(account)) + "|" + strings.TrimSpace(channel)
}
func SendPlatformLoginCode(account, channel string) error {
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
if account == "" {
return errors.New("账号不能为空")
}
if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码")
}
var u models.AdminUser
if err := models.Orm.QueryTable(new(models.AdminUser)).Filter("account", account).One(&u); err != nil {
return errors.New("用户不存在")
}
if u.Status == 0 {
return errors.New("账号已禁用")
}
if channel == "sms" && (u.Phone == nil || strings.TrimSpace(*u.Phone) == "") {
return errors.New("该账号未绑定手机号")
}
if channel == "email" && (u.Email == nil || strings.TrimSpace(*u.Email) == "") {
return errors.New("该账号未绑定邮箱")
}
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
loginCodeStore.Store(codeKey(account, channel), loginCodeItem{
Code: code,
Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute),
})
return nil
}
func VerifyPlatformLoginCode(account, channel, code string) error {
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
code = strings.TrimSpace(code)
if account == "" || code == "" {
return errors.New("验证码不能为空")
}
val, ok := loginCodeStore.Load(codeKey(account, channel))
if !ok {
return errors.New("验证码不存在或已失效")
}
item, ok := val.(loginCodeItem)
if !ok {
return errors.New("验证码状态异常")
}
if time.Now().After(item.ExpiredAt) {
loginCodeStore.Delete(codeKey(account, channel))
return errors.New("验证码已过期")
}
if item.Code != code {
return errors.New("验证码错误")
}
loginCodeStore.Delete(codeKey(account, channel))
return nil
}
func SendBackendLoginCode(tenantName, account, channel string) error {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
channel = strings.TrimSpace(channel)
if tenantName == "" || account == "" {
return errors.New("租户名称和账号不能为空")
}
if channel != "sms" && channel != "email" {
return errors.New("仅支持短信或邮箱验证码")
}
tenant, err := findTenantByLoginName(tenantName)
if err != nil {
return errors.New("租户不存在")
}
rand.Seed(time.Now().UnixNano())
code := fmt.Sprintf("%06d", rand.Intn(1000000))
switch channel {
case "sms":
phone := account
var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("phone", phone).
One(&user); err != nil {
return errors.New("该手机号非当前企业绑定号码,请重试")
}
if user.Status == 0 {
return errors.New("账号已禁用")
}
if user.Phone == nil || strings.TrimSpace(*user.Phone) == "" {
return errors.New("该手机号非当前企业绑定号码,请重试")
}
content := "短信验证码:" + code
if err := enqueueSMSTaskForLogin(tenant.ID, phone, content, code); err != nil {
return errors.New("短信发送失败,请重试")
}
case "email":
email := account
var user models.SystemTenantUser
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("email", email).
One(&user); err != nil {
return errors.New("该账号未绑定邮箱")
}
if user.Status == 0 {
return errors.New("账号已禁用")
}
if user.Email == nil || strings.TrimSpace(*user.Email) == "" {
return errors.New("该账号未绑定邮箱")
}
}
loginCodeStore.Store(codeKey(tenantName+"#"+account, channel), loginCodeItem{
Code: code,
Channel: channel,
ExpiredAt: time.Now().Add(5 * time.Minute),
})
return nil
}
func VerifyBackendLoginCode(tenantName, account, channel, code string) error {
return VerifyPlatformLoginCode(tenantName+"#"+account, channel, code)
}
func getDefaultSystemSMSConfig() (backendURL string, apiKey string, err error) {
backendURL = models.GetPlatformSettingValue("sms_custom_url", "")
apiKey = models.GetPlatformSettingValue("sms_custom_key", "")
if backendURL == "" || apiKey == "" {
return "", "", fmt.Errorf("短信网关未配置")
}
return backendURL, apiKey, nil
}
// enqueueSMSTaskForLogin 入队短信任务到网关,并写入 yz_system_sms_tasks
func enqueueSMSTaskForLogin(tid uint64, phone, content, code string) error {
backendURL, apiKey, err := getDefaultSystemSMSConfig()
if err != nil {
return err
}
if backendURL == "" || apiKey == "" {
return errors.New("短信网关未配置")
}
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 {
return err
}
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 {
return err
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
bodyStr := strings.TrimSpace(string(bodyBytes))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("gateway http status: %d, body: %s", resp.StatusCode, bodyStr)
}
now := time.Now()
tidCopy := tid
contentPtr := content
var reportPtr *string
if bodyStr != "" {
reportPtr = &bodyStr
}
task := &models.SystemSMSTask{
Tid: &tidCopy,
ApiKey: apiKey,
Phone: phone,
Content: &contentPtr,
Status: 3,
Code: code,
ReportRaw: reportPtr,
CreateTime: &now,
UpdateTime: &now,
}
_, insertErr := models.Orm.Insert(task)
if insertErr != nil {
return nil
}
return nil
}
+155 -158
View File
@@ -1,158 +1,155 @@
package services
import (
"errors"
"strings"
"server/models"
"server/pkg/jwtutil"
"server/pkg/passwordutil"
)
type PlatformLoginUser struct {
ID uint64
Account string
Name string
Tid uint64
Rid uint64
Avatar string
RoleName string
}
func adminRoleNameByID(roleID uint64) string {
if roleID == 0 {
return ""
}
var role models.AdminRole
err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", roleID).One(&role)
if err != nil {
return ""
}
return role.Name
}
func toPlatformLoginUser(user *models.AdminUser) *PlatformLoginUser {
name := ""
if user.Name != nil {
name = *user.Name
}
avatar := ""
if user.Avatar != nil {
avatar = *user.Avatar
}
return &PlatformLoginUser{
ID: user.ID,
Account: user.Account,
Name: name,
Tid: 0,
Rid: user.RoleID,
Avatar: avatar,
RoleName: adminRoleNameByID(user.RoleID),
}
}
// PlatformAdminLogin 平台端登录:仅校验 yz_system_admin_user,不需要租户。
func PlatformAdminLogin(account, password string) (string, *PlatformLoginUser, error) {
account = strings.TrimSpace(account)
password = strings.TrimSpace(password)
if account == "" || password == "" {
return "", nil, errors.New("用户名或密码不能为空")
}
var user models.AdminUser
err := models.Orm.QueryTable(new(models.AdminUser)).
Filter("account", account).
One(&user)
if err != nil {
return "", nil, errors.New("用户名或密码错误")
}
if user.Status == 0 {
return "", nil, errors.New("账号已禁用")
}
if !passwordutil.Verify(user.Password, password) {
return "", nil, errors.New("用户名或密码错误")
}
const tenantID = 0
const userType = "platform"
token, err := jwtutil.GenerateToken(int(user.ID), user.Account, tenantID, userType)
if err != nil {
return "", nil, err
}
loginUser := toPlatformLoginUser(&user)
return token, loginUser, nil
}
// BackendLogin backend 登录:先校验租户,再校验租户下用户账号和密码。
func BackendLogin(tenantName, account, password string) (string, *PlatformLoginUser, error) {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
password = strings.TrimSpace(password)
if tenantName == "" || account == "" || password == "" {
return "", nil, errors.New("租户名称、用户名或密码不能为空")
}
var tenant models.SystemTenant
err := models.Orm.QueryTable(new(models.SystemTenant)).
Filter("tenant_name", tenantName).
One(&tenant)
if err != nil {
return "", nil, errors.New("租户不存在")
}
if tenant.Status != 1 {
return "", nil, errors.New("租户已停用")
}
var tenantUser models.SystemTenantUser
err = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("account", account).
One(&tenantUser)
if err != nil {
return "", nil, errors.New("用户名或密码错误")
}
if tenantUser.Status == 0 {
return "", nil, errors.New("账号已禁用")
}
if tenantUser.Password == nil || !passwordutil.Verify(*tenantUser.Password, password) {
return "", nil, errors.New("用户名或密码错误")
}
tenantID := int(tenant.ID)
const userType = "backend"
token, err := jwtutil.GenerateToken(int(tenantUser.Uid), account, tenantID, userType)
if err != nil {
return "", nil, err
}
loginUser := &PlatformLoginUser{
ID: tenantUser.Uid,
Account: account,
Name: "",
Tid: tenant.ID,
Rid: 0,
Avatar: "",
RoleName: "",
}
if tenantUser.Account != nil && strings.TrimSpace(*tenantUser.Account) != "" {
loginUser.Account = strings.TrimSpace(*tenantUser.Account)
}
if tenantUser.Name != nil {
loginUser.Name = strings.TrimSpace(*tenantUser.Name)
}
return token, loginUser, nil
}
// PlatformGetCurrentUser 根据平台管理员用户 ID 返回登录用户信息(含角色名称)。
func PlatformGetCurrentUser(uid uint64) (*PlatformLoginUser, error) {
u, err := GetAdminUserByID(uid)
if err != nil {
return nil, errors.New("用户不存在")
}
if u.Status == 0 {
return nil, errors.New("账号已禁用")
}
return toPlatformLoginUser(u), nil
}
package services
import (
"errors"
"strings"
"server/models"
"server/pkg/jwtutil"
"server/pkg/passwordutil"
)
type PlatformLoginUser struct {
ID uint64
Account string
Name string
Tid uint64
Rid uint64
Avatar string
RoleName string
}
func adminRoleNameByID(roleID uint64) string {
if roleID == 0 {
return ""
}
var role models.AdminRole
err := models.Orm.QueryTable(new(models.AdminRole)).Filter("id", roleID).One(&role)
if err != nil {
return ""
}
return role.Name
}
func toPlatformLoginUser(user *models.AdminUser) *PlatformLoginUser {
name := ""
if user.Name != nil {
name = *user.Name
}
avatar := ""
if user.Avatar != nil {
avatar = *user.Avatar
}
return &PlatformLoginUser{
ID: user.ID,
Account: user.Account,
Name: name,
Tid: 0,
Rid: user.RoleID,
Avatar: avatar,
RoleName: adminRoleNameByID(user.RoleID),
}
}
// PlatformAdminLogin 平台端登录:仅校验 yz_system_admin_user,不需要租户。
func PlatformAdminLogin(account, password string) (string, *PlatformLoginUser, error) {
account = strings.TrimSpace(account)
password = strings.TrimSpace(password)
if account == "" || password == "" {
return "", nil, errors.New("用户名或密码不能为空")
}
var user models.AdminUser
err := models.Orm.QueryTable(new(models.AdminUser)).
Filter("account", account).
One(&user)
if err != nil {
return "", nil, errors.New("用户名或密码错误")
}
if user.Status == 0 {
return "", nil, errors.New("账号已禁用")
}
if !passwordutil.Verify(user.Password, password) {
return "", nil, errors.New("用户名或密码错误")
}
const tenantID = 0
const userType = "platform"
token, err := jwtutil.GenerateToken(int(user.ID), user.Account, tenantID, userType)
if err != nil {
return "", nil, err
}
loginUser := toPlatformLoginUser(&user)
return token, loginUser, nil
}
// BackendLogin backend 登录:先校验租户,再校验租户下用户账号和密码。
func BackendLogin(tenantName, account, password string) (string, *PlatformLoginUser, error) {
tenantName = strings.TrimSpace(tenantName)
account = strings.TrimSpace(account)
password = strings.TrimSpace(password)
if tenantName == "" || account == "" || password == "" {
return "", nil, errors.New("租户名称、用户名或密码不能为空")
}
tenant, err := findTenantByLoginName(tenantName)
if err != nil {
return "", nil, errors.New("租户不存在")
}
if tenant.Status != 1 {
return "", nil, errors.New("租户已停用")
}
var tenantUser models.SystemTenantUser
err = models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("tid", tenant.ID).
Filter("account", account).
One(&tenantUser)
if err != nil {
return "", nil, errors.New("用户名或密码错误")
}
if tenantUser.Status == 0 {
return "", nil, errors.New("账号已禁用")
}
if tenantUser.Password == nil || !passwordutil.Verify(*tenantUser.Password, password) {
return "", nil, errors.New("用户名或密码错误")
}
tenantID := int(tenant.ID)
const userType = "backend"
token, err := jwtutil.GenerateToken(int(tenantUser.Uid), account, tenantID, userType)
if err != nil {
return "", nil, err
}
loginUser := &PlatformLoginUser{
ID: tenantUser.Uid,
Account: account,
Name: "",
Tid: tenant.ID,
Rid: 0,
Avatar: "",
RoleName: "",
}
if tenantUser.Account != nil && strings.TrimSpace(*tenantUser.Account) != "" {
loginUser.Account = strings.TrimSpace(*tenantUser.Account)
}
if tenantUser.Name != nil {
loginUser.Name = strings.TrimSpace(*tenantUser.Name)
}
return token, loginUser, nil
}
// PlatformGetCurrentUser 根据平台管理员用户 ID 返回登录用户信息(含角色名称)。
func PlatformGetCurrentUser(uid uint64) (*PlatformLoginUser, error) {
u, err := GetAdminUserByID(uid)
if err != nil {
return nil, errors.New("用户不存在")
}
if u.Status == 0 {
return nil, errors.New("账号已禁用")
}
return toPlatformLoginUser(u), nil
}
+43
View File
@@ -0,0 +1,43 @@
package services
import (
"strings"
"github.com/beego/beego/v2/client/orm"
"server/models"
)
/* findTenantByLoginName 根据租户简称、短名或编码查找租户。
*
* 当前表结构中 tenant_name 的业务含义就是“租户简称”,同时兼容
* tenant_short_name 和 tenant_code 两个历史/备用登录标识。
*/
func findTenantByLoginName(loginName string) (*models.SystemTenant, error) {
loginName = strings.TrimSpace(loginName)
if loginName == "" {
return nil, orm.ErrNoRows
}
// tenant_name 的业务含义是租户简称。使用 TRIM 兼容历史数据中字段值
// 前后存在空格的情况;参数通过 SetArgs 绑定,避免 SQL 注入。
tenant := &models.SystemTenant{}
query := `
SELECT id, tenant_code, tenant_name, tenant_short_name,
contact_person, contact_phone, contact_email, address,
worktime, status, remark, create_time, update_time, delete_time
FROM yz_system_tenant
WHERE (TRIM(tenant_name) = TRIM(?) OR
TRIM(tenant_short_name) = TRIM(?) OR
TRIM(tenant_code) = TRIM(?))
AND status <> 0
ORDER BY id ASC
LIMIT 1`
err := models.Orm.Raw(query).
SetArgs(loginName, loginName, loginName).
QueryRow(tenant)
if err != nil {
return nil, err
}
return tenant, nil
}