mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-08-28 05:12:32 +08:00
版本预发布
This commit is contained in:
@@ -96,6 +96,19 @@ func (s *sSysConfig) GetSmtp(ctx context.Context) (conf *model.EmailConfig, err
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// GetBasic 获取基础配置
|
||||
func (s *sSysConfig) GetBasic(ctx context.Context) (conf *model.BasicConfig, err error) {
|
||||
models, err := s.GetConfigByGroup(ctx, sysin.GetConfigInp{Group: "basic"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = gconv.Struct(models.List, &conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// GetLoadSSL 获取本地日志配置
|
||||
func (s *sSysConfig) GetLoadSSL(ctx context.Context) (conf *model.SSLConfig, err error) {
|
||||
if err = g.Cfg().MustGet(ctx, "hotgo.ssl").Struct(&conf); err != nil {
|
||||
|
@@ -8,14 +8,15 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/crons"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"hotgo/utility/validate"
|
||||
@@ -33,10 +34,7 @@ func init() {
|
||||
}
|
||||
|
||||
func (s *sSysCron) StartCron(ctx context.Context) {
|
||||
var (
|
||||
list []*entity.SysCron
|
||||
)
|
||||
|
||||
var list []*entity.SysCron
|
||||
if err := dao.SysCron.Ctx(ctx).
|
||||
Where("status", consts.StatusEnabled).
|
||||
Order("sort asc,id desc").
|
||||
@@ -49,17 +47,30 @@ func (s *sSysCron) StartCron(ctx context.Context) {
|
||||
g.Log().Fatalf(ctx, "定时任务启动失败, err . %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Delete 删除
|
||||
func (s *sSysCron) Delete(ctx context.Context, in sysin.CronDeleteInp) error {
|
||||
_, err := dao.SysCron.Ctx(ctx).Where("id", in.Id).Delete()
|
||||
if err != nil {
|
||||
func (s *sSysCron) Delete(ctx context.Context, in sysin.CronDeleteInp) (err error) {
|
||||
var models *entity.SysCron
|
||||
if err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Scan(&models); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
if models == nil {
|
||||
err = gerror.New("定时任务不存在或已被删除")
|
||||
return
|
||||
}
|
||||
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
_, err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Delete()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
return crons.Delete(models)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -67,34 +78,30 @@ func (s *sSysCron) Delete(ctx context.Context, in sysin.CronDeleteInp) error {
|
||||
func (s *sSysCron) Edit(ctx context.Context, in sysin.CronEditInp) (err error) {
|
||||
if in.Name == "" {
|
||||
err = gerror.New("标题不能为空")
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
// 修改
|
||||
in.UpdatedAt = gtime.Now()
|
||||
if in.Id > 0 {
|
||||
_, err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Data(in).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
_, err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Data(in).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return crons.RefreshStatus(&in.SysCron)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
in.CreatedAt = gtime.Now()
|
||||
_, err = dao.SysCron.Ctx(ctx).Data(in).Insert()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新部门状态
|
||||
func (s *sSysCron) Status(ctx context.Context, in sysin.CronStatusInp) (err error) {
|
||||
|
||||
if in.Id <= 0 {
|
||||
err = gerror.New("ID不能为空")
|
||||
return err
|
||||
@@ -110,36 +117,46 @@ func (s *sSysCron) Status(ctx context.Context, in sysin.CronStatusInp) (err erro
|
||||
return err
|
||||
}
|
||||
|
||||
// 修改
|
||||
in.UpdatedAt = gtime.Now()
|
||||
_, err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Data("status", in.Status).Update()
|
||||
if err != nil {
|
||||
var models *entity.SysCron
|
||||
if err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Scan(&models); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
if models == nil {
|
||||
err = gerror.New("定时任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
_, err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Data("status", in.Status).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
models.Status = in.Status
|
||||
return crons.RefreshStatus(models)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// MaxSort 最大排序
|
||||
func (s *sSysCron) MaxSort(ctx context.Context, in sysin.CronMaxSortInp) (*sysin.CronMaxSortModel, error) {
|
||||
var res sysin.CronMaxSortModel
|
||||
|
||||
if in.Id > 0 {
|
||||
if err := dao.SysCron.Ctx(ctx).Where("id", in.Id).Order("sort desc").Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return nil, err
|
||||
}
|
||||
func (s *sSysCron) MaxSort(ctx context.Context, in sysin.CronMaxSortInp) (res *sysin.CronMaxSortModel, err error) {
|
||||
if err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Order("sort desc").Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return nil, err
|
||||
}
|
||||
if res == nil {
|
||||
res = new(sysin.CronMaxSortModel)
|
||||
}
|
||||
|
||||
res.Sort = res.Sort + 10
|
||||
|
||||
return &res, nil
|
||||
res.Sort = form.DefaultMaxSort(ctx, res.Sort)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// View 获取指定字典类型信息
|
||||
func (s *sSysCron) View(ctx context.Context, in sysin.CronViewInp) (res *sysin.CronViewModel, err error) {
|
||||
|
||||
if err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return nil, err
|
||||
@@ -152,12 +169,10 @@ func (s *sSysCron) View(ctx context.Context, in sysin.CronViewInp) (res *sysin.C
|
||||
func (s *sSysCron) List(ctx context.Context, in sysin.CronListInp) (list []*sysin.CronListModel, totalCount int, err error) {
|
||||
mod := dao.SysCron.Ctx(ctx)
|
||||
|
||||
// 访问路径
|
||||
if in.Name != "" {
|
||||
mod = mod.WhereLike("name", "%"+in.Name+"%")
|
||||
}
|
||||
|
||||
// 请求方式
|
||||
if in.Status > 0 {
|
||||
mod = mod.Where("status", in.Status)
|
||||
}
|
||||
@@ -180,7 +195,6 @@ func (s *sSysCron) List(ctx context.Context, in sysin.CronListInp) (list []*sysi
|
||||
for _, v := range list {
|
||||
v.GroupName, _ = dao.SysCronGroup.GetName(ctx, v.GroupId)
|
||||
}
|
||||
|
||||
return list, totalCount, err
|
||||
}
|
||||
|
||||
@@ -197,6 +211,5 @@ func (s *sSysCron) OnlineExec(ctx context.Context, in sysin.OnlineExecInp) (err
|
||||
}
|
||||
|
||||
newCtx := context.WithValue(gctx.New(), consts.ContextKeyCronArgs, strings.Split(data.Params, consts.CronSplitStr))
|
||||
|
||||
return crons.Once(newCtx, data)
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
// @AutoGenerate Version 2.1.0
|
||||
// @AutoGenerate Date 2023-01-18 15:19:42
|
||||
// @AutoGenerate Version 2.1.2
|
||||
// @AutoGenerate Date 2023-02-08 17:47:32
|
||||
//
|
||||
package sys
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/hgorm"
|
||||
"hotgo/internal/library/hgorm/handler"
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
@@ -39,14 +40,14 @@ func init() {
|
||||
service.RegisterSysCurdDemo(NewSysCurdDemo())
|
||||
}
|
||||
|
||||
// Model 生成演示Orm模型
|
||||
func (s *sSysCurdDemo) Model(ctx context.Context) *gdb.Model {
|
||||
return dao.SysGenCurdDemo.Ctx(ctx)
|
||||
// Model 生成演示ORM模型
|
||||
func (s *sSysCurdDemo) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
return handler.Model(dao.SysGenCurdDemo.Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
// List 获取生成演示列表
|
||||
func (s *sSysCurdDemo) List(ctx context.Context, in sysin.CurdDemoListInp) (list []*sysin.CurdDemoListModel, totalCount int, err error) {
|
||||
mod := dao.SysGenCurdDemo.Ctx(ctx)
|
||||
mod := s.Model(ctx)
|
||||
|
||||
// 查询ID
|
||||
if in.Id > 0 {
|
||||
@@ -77,36 +78,37 @@ func (s *sSysCurdDemo) List(ctx context.Context, in sysin.CurdDemoListInp) (list
|
||||
totalCount, err = mod.Clone().Count(1)
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return list, totalCount, err
|
||||
return
|
||||
}
|
||||
|
||||
if totalCount == 0 {
|
||||
return list, totalCount, nil
|
||||
return
|
||||
}
|
||||
|
||||
//关联表select
|
||||
fields, err := hgorm.GenJoinSelect(ctx, sysin.CurdDemoListModel{}, dao.SysGenCurdDemo, []*hgorm.Join{
|
||||
{Dao: dao.TestCategory, Alias: "testCategory"},
|
||||
})
|
||||
if err = mod.Fields(fields).Handler(hgorm.HandlerFilterAuth).Page(in.Page, in.PerPage).OrderAsc(dao.SysGenCurdDemo.Columns().Sort).OrderDesc(dao.SysGenCurdDemo.Columns().Id).Scan(&list); err != nil {
|
||||
|
||||
if err = mod.Fields(fields).Page(in.Page, in.PerPage).OrderAsc(dao.SysGenCurdDemo.Columns().Sort).OrderDesc(dao.SysGenCurdDemo.Columns().Id).Scan(&list); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return list, totalCount, err
|
||||
return
|
||||
}
|
||||
|
||||
return list, totalCount, err
|
||||
return
|
||||
}
|
||||
|
||||
// Export 导出生成演示
|
||||
func (s *sSysCurdDemo) Export(ctx context.Context, in sysin.CurdDemoListInp) (err error) {
|
||||
list, totalCount, err := s.List(ctx, in)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
|
||||
tags, err := convert.GetEntityDescTags(sysin.CurdDemoExportModel{})
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -115,13 +117,11 @@ func (s *sSysCurdDemo) Export(ctx context.Context, in sysin.CurdDemoListInp) (er
|
||||
exports []sysin.CurdDemoExportModel
|
||||
)
|
||||
|
||||
err = gconv.Scan(list, &exports)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName); err != nil {
|
||||
if err = gconv.Scan(list, &exports); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func (s *sSysCurdDemo) Edit(ctx context.Context, in sysin.CurdDemoEditInp) (err
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
in.UpdatedBy = contexts.GetUserId(ctx)
|
||||
_, err = dao.SysGenCurdDemo.Ctx(ctx).
|
||||
_, err = s.Model(ctx).
|
||||
FieldsEx(
|
||||
dao.SysGenCurdDemo.Columns().Id,
|
||||
dao.SysGenCurdDemo.Columns().CreatedBy,
|
||||
@@ -138,85 +138,70 @@ func (s *sSysCurdDemo) Edit(ctx context.Context, in sysin.CurdDemoEditInp) (err
|
||||
dao.SysGenCurdDemo.Columns().DeletedAt,
|
||||
).
|
||||
Where(dao.SysGenCurdDemo.Columns().Id, in.Id).Data(in).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
// 新增
|
||||
in.CreatedBy = contexts.GetUserId(ctx)
|
||||
_, err = dao.SysGenCurdDemo.Ctx(ctx).
|
||||
_, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
FieldsEx(
|
||||
dao.SysGenCurdDemo.Columns().Id,
|
||||
dao.SysGenCurdDemo.Columns().UpdatedBy,
|
||||
dao.SysGenCurdDemo.Columns().DeletedAt,
|
||||
).
|
||||
Data(in).Insert()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除生成演示
|
||||
func (s *sSysCurdDemo) Delete(ctx context.Context, in sysin.CurdDemoDeleteInp) (err error) {
|
||||
_, err = dao.SysGenCurdDemo.Ctx(ctx).Where(dao.SysGenCurdDemo.Columns().Id, in.Id).Delete()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err = s.Model(ctx).Where(dao.SysGenCurdDemo.Columns().Id, in.Id).Delete()
|
||||
return
|
||||
}
|
||||
|
||||
// MaxSort 获取生成演示最大排序
|
||||
func (s *sSysCurdDemo) MaxSort(ctx context.Context, in sysin.CurdDemoMaxSortInp) (res *sysin.CurdDemoMaxSortModel, err error) {
|
||||
if err = dao.SysGenCurdDemo.Ctx(ctx).Fields(dao.SysGenCurdDemo.Columns().Sort).OrderDesc(dao.SysGenCurdDemo.Columns().Sort).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
|
||||
res.Sort = res.Sort + g.Cfg().MustGet(ctx, "hotgo.admin.maxSortIncrement").Int()
|
||||
return res, nil
|
||||
if res == nil {
|
||||
res = new(sysin.CurdDemoMaxSortModel)
|
||||
}
|
||||
|
||||
res.Sort = form.DefaultMaxSort(ctx, res.Sort)
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取生成演示指定信息
|
||||
func (s *sSysCurdDemo) View(ctx context.Context, in sysin.CurdDemoViewInp) (res *sysin.CurdDemoViewModel, err error) {
|
||||
if err = dao.SysGenCurdDemo.Ctx(ctx).Where(dao.SysGenCurdDemo.Columns().Id, in.Id).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
err = s.Model(ctx).Where(dao.SysGenCurdDemo.Columns().Id, in.Id).Scan(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新生成演示状态
|
||||
func (s *sSysCurdDemo) Status(ctx context.Context, in sysin.CurdDemoStatusInp) (err error) {
|
||||
if in.Id <= 0 {
|
||||
err = gerror.New("ID不能为空")
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
if in.Status <= 0 {
|
||||
err = gerror.New("状态不能为空")
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
if !validate.InSliceInt(consts.StatusMap, in.Status) {
|
||||
err = gerror.New("状态不正确")
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
_, err = dao.SysGenCurdDemo.Ctx(ctx).Where(dao.SysGenCurdDemo.Columns().Id, in.Id).Data(dao.SysGenCurdDemo.Columns().Status, in.Status).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err = s.Model(ctx).Where(dao.SysGenCurdDemo.Columns().Id, in.Id).Data(g.Map{
|
||||
dao.SysGenCurdDemo.Columns().Status: in.Status,
|
||||
dao.SysGenCurdDemo.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
||||
}).Update()
|
||||
return
|
||||
}
|
||||
|
||||
// Switch 更新生成演示开关
|
||||
@@ -229,14 +214,12 @@ func (s *sSysCurdDemo) Switch(ctx context.Context, in sysin.CurdDemoSwitchInp) (
|
||||
|
||||
if !validate.InSliceString(fields, in.Key) {
|
||||
err = gerror.New("开关键名不在白名单")
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
_, err = dao.SysGenCurdDemo.Ctx(ctx).Where(dao.SysGenCurdDemo.Columns().Id, in.Id).Data(in.Key, in.Value).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err = s.Model(ctx).Where(dao.SysGenCurdDemo.Columns().Id, in.Id).Data(g.Map{
|
||||
in.Key: in.Value,
|
||||
dao.SysGenCurdDemo.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
||||
}).Update()
|
||||
return
|
||||
}
|
||||
|
443
server/internal/logic/sys/ems_log.go
Normal file
443
server/internal/logic/sys/ems_log.go
Normal file
@@ -0,0 +1,443 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2022 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
//
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/os/gview"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/grand"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/ems"
|
||||
"hotgo/internal/library/location"
|
||||
"hotgo/internal/model"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"hotgo/utility/charset"
|
||||
"hotgo/utility/useragent"
|
||||
"hotgo/utility/validate"
|
||||
"time"
|
||||
)
|
||||
|
||||
type sSysEmsLog struct{}
|
||||
|
||||
func NewSysEmsLog() *sSysEmsLog {
|
||||
return &sSysEmsLog{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysEmsLog(NewSysEmsLog())
|
||||
}
|
||||
|
||||
// Delete 删除
|
||||
func (s *sSysEmsLog) Delete(ctx context.Context, in sysin.EmsLogDeleteInp) error {
|
||||
_, err := dao.SysEmsLog.Ctx(ctx).Where("id", in.Id).Delete()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edit 修改/新增
|
||||
func (s *sSysEmsLog) Edit(ctx context.Context, in sysin.EmsLogEditInp) (err error) {
|
||||
if in.Ip == "" {
|
||||
err = gerror.New("ip不能为空")
|
||||
return err
|
||||
}
|
||||
|
||||
// 修改
|
||||
if in.Id > 0 {
|
||||
_, err = dao.SysEmsLog.Ctx(ctx).Where("id", in.Id).Data(in).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 新增
|
||||
_, err = dao.SysEmsLog.Ctx(ctx).Data(in).Insert()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status 更新部门状态
|
||||
func (s *sSysEmsLog) Status(ctx context.Context, in sysin.EmsLogStatusInp) (err error) {
|
||||
if in.Id <= 0 {
|
||||
err = gerror.New("ID不能为空")
|
||||
return err
|
||||
}
|
||||
|
||||
if in.Status <= 0 {
|
||||
err = gerror.New("状态不能为空")
|
||||
return err
|
||||
}
|
||||
|
||||
if !validate.InSliceInt(consts.StatusMap, in.Status) {
|
||||
err = gerror.New("状态不正确")
|
||||
return err
|
||||
}
|
||||
|
||||
// 修改
|
||||
_, err = dao.SysEmsLog.Ctx(ctx).Where("id", in.Id).Data("status", in.Status).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// View 获取指定字典类型信息
|
||||
func (s *sSysEmsLog) View(ctx context.Context, in sysin.EmsLogViewInp) (res *sysin.EmsLogViewModel, err error) {
|
||||
if err = dao.SysEmsLog.Ctx(ctx).Where("id", in.Id).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// List 获取列表
|
||||
func (s *sSysEmsLog) List(ctx context.Context, in sysin.EmsLogListInp) (list []*sysin.EmsLogListModel, totalCount int, err error) {
|
||||
mod := dao.SysEmsLog.Ctx(ctx)
|
||||
|
||||
if in.Status > 0 {
|
||||
mod = mod.Where("status", in.Status)
|
||||
}
|
||||
|
||||
totalCount, err = mod.Count()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return list, totalCount, err
|
||||
}
|
||||
|
||||
if totalCount == 0 {
|
||||
return list, totalCount, nil
|
||||
}
|
||||
|
||||
if err = mod.Page(in.Page, in.PerPage).Order("id desc").Scan(&list); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return list, totalCount, err
|
||||
}
|
||||
|
||||
return list, totalCount, err
|
||||
}
|
||||
|
||||
// Send 发送邮件
|
||||
func (s *sSysEmsLog) Send(ctx context.Context, in sysin.SendEmsInp) (err error) {
|
||||
var models *entity.SysEmsLog
|
||||
if err = dao.SysEmsLog.Ctx(ctx).Where("event", in.Event).Where("email", in.Email).Scan(&models); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := service.SysConfig().GetSmtp(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in.Template, err = s.GetTemplate(ctx, in.Event, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.AllowSend(ctx, models, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if consts.IsCodeEmsTemplate(in.Event) && in.Code == "" {
|
||||
in.Code = grand.Digits(4)
|
||||
}
|
||||
|
||||
view, err := s.newView(ctx, in, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if in.TplData == nil {
|
||||
in.TplData = make(g.Map)
|
||||
}
|
||||
|
||||
switch in.Event {
|
||||
// 富文本
|
||||
case consts.EmsTemplateText:
|
||||
if in.Content == "" {
|
||||
err = gerror.New("富文本类型邮件内容不能为空")
|
||||
return
|
||||
}
|
||||
in.TplData["content"] = in.Content
|
||||
in.Content, err = view.Parse(ctx, in.Template, in.TplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 验证码、重置密码类
|
||||
default:
|
||||
in.Content, err = view.Parse(ctx, in.Template, in.TplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
subject, ok := consts.EmsSubjectMap[in.Event]
|
||||
if !ok {
|
||||
subject = "HotGo"
|
||||
}
|
||||
|
||||
err = ems.Send(config, in.Email, subject, in.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data = new(entity.SysEmsLog)
|
||||
data.Event = in.Event
|
||||
data.Email = in.Email
|
||||
data.Content = in.Content
|
||||
data.Code = in.Code
|
||||
data.Ip = location.GetClientIp(ghttp.RequestFromCtx(ctx))
|
||||
data.Status = consts.EmsStatusNotUsed
|
||||
data.CreatedAt = gtime.Now()
|
||||
data.UpdatedAt = gtime.Now()
|
||||
|
||||
_, err = dao.SysEmsLog.Ctx(ctx).Data(data).Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysEmsLog) newView(ctx context.Context, in sysin.SendEmsInp, config *model.EmailConfig) (view *gview.View, err error) {
|
||||
view = gview.New()
|
||||
err = view.SetConfig(gview.Config{
|
||||
Delimiters: g.Cfg().MustGet(ctx, "viewer.delimiters").Strings(),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 富文本为自定义内容,可能不需要变量
|
||||
if in.Event == consts.EmsTemplateText {
|
||||
return
|
||||
}
|
||||
var (
|
||||
username string
|
||||
user = contexts.GetUser(ctx)
|
||||
request = ghttp.RequestFromCtx(ctx)
|
||||
ip = location.GetClientIp(request)
|
||||
)
|
||||
|
||||
loc, err := location.GetLocation(ctx, ip)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if loc == nil {
|
||||
loc = new(location.IpLocationData)
|
||||
}
|
||||
|
||||
cityLabel, err := location.ParseRegion(ctx, loc.ProvinceCode, loc.CityCode, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
basic, err := service.SysConfig().GetBasic(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if basic == nil {
|
||||
basic = new(model.BasicConfig)
|
||||
basic.Name = "HotGo"
|
||||
basic.Domain = "https://hotgo.facms.cn"
|
||||
basic.Logo = "http://bufanyun.cn-bj.ufileos.com/haoka/attachment/images/2023-02-04/cq9kf7s66jt7hkpvbh.png"
|
||||
basic.SystemOpen = true
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
username = user.Username
|
||||
}
|
||||
|
||||
// 公共变量
|
||||
view.Assigns(gview.Params{
|
||||
"code": in.Code, // 验证码
|
||||
"expires": config.CodeExpire / 60, // 验证码有效期(分钟)
|
||||
"username": username, // 发送者用户名
|
||||
"name": basic.Name, // 网站名称
|
||||
"logo": basic.Logo, // 网站logo
|
||||
"domain": basic.Domain, // 网站域名
|
||||
"github": "https://github.com/bufanyun/hotgo", // github
|
||||
"os": useragent.GetOs(request.Header.Get("User-Agent")), // 发送者操作系统
|
||||
"ip": gstr.HideStr(ip, 30, `*`), // 发送者IP
|
||||
"cityLabel": cityLabel, // IP归属地
|
||||
})
|
||||
|
||||
// 重置密码
|
||||
if in.Event == consts.EmsTemplateResetPwd {
|
||||
var (
|
||||
passwordResetLink string
|
||||
resetToken = charset.RandomCreateBytes(32)
|
||||
)
|
||||
if user != nil {
|
||||
switch user.App {
|
||||
// 后台用户
|
||||
case consts.AppAdmin:
|
||||
_, err = g.Model("admin_member").Ctx(ctx).Where("id", user.Id).Data(g.Map{"password_reset_token": resetToken}).Update()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
passwordResetLink = fmt.Sprintf("%s/admin/passwordReset?token=%s", basic.Domain, resetToken)
|
||||
// 前台用户
|
||||
case consts.AppApi:
|
||||
// ...
|
||||
}
|
||||
}
|
||||
view.Assign("passwordResetLink", passwordResetLink)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetTemplate 获取指定邮件模板
|
||||
func (s *sSysEmsLog) GetTemplate(ctx context.Context, template string, config *model.EmailConfig) (val string, err error) {
|
||||
if template == "" {
|
||||
return "", gerror.New("模板不能为空")
|
||||
}
|
||||
if config == nil {
|
||||
config, err = service.SysConfig().GetSmtp(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if len(config.Template) == 0 {
|
||||
return "", gerror.New("管理员还没有配置任何模板!")
|
||||
}
|
||||
|
||||
for _, v := range config.Template {
|
||||
if v.Key == template {
|
||||
return v.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AllowSend 是否允许发送
|
||||
func (s *sSysEmsLog) AllowSend(ctx context.Context, models *entity.SysEmsLog, config *model.EmailConfig) (err error) {
|
||||
if models == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 富文本事件不限制
|
||||
if models.Event == consts.EmsTemplateText {
|
||||
return nil
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
config, err = service.SysConfig().GetSmtp(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if gtime.Now().Before(models.CreatedAt.Add(time.Second * time.Duration(config.MinInterval))) {
|
||||
return gerror.New("发送频繁,请稍后再试!")
|
||||
}
|
||||
|
||||
if config.MaxIpLimit > 0 {
|
||||
count, err := dao.SysEmsLog.NowDayCount(ctx, models.Event, models.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count >= config.MaxIpLimit {
|
||||
return gerror.New("今天发送短信过多,请次日后再试!")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// VerifyCode 效验验证码
|
||||
func (s *sSysEmsLog) VerifyCode(ctx context.Context, in sysin.VerifyEmsCodeInp) (err error) {
|
||||
if in.Event == "" {
|
||||
return gerror.New("事件不能为空")
|
||||
}
|
||||
if in.Email == "" {
|
||||
return gerror.New("邮箱不能为空")
|
||||
}
|
||||
if in.Event == consts.EmsTemplateResetPwd || in.Event == consts.EmsTemplateText {
|
||||
return gerror.Newf("事件类型无需验证:%v", in.Event)
|
||||
}
|
||||
|
||||
config, err := service.SysConfig().GetSmtp(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
models *entity.SysEmsLog
|
||||
)
|
||||
if err = dao.SysEmsLog.Ctx(ctx).Where("event", in.Event).Where("email", in.Email).Order("id desc").Scan(&models); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
if models == nil {
|
||||
return gerror.New("验证码错误")
|
||||
}
|
||||
|
||||
if models.Times >= 10 {
|
||||
return gerror.New("验证码错误次数过多,请重新发送!")
|
||||
}
|
||||
|
||||
if in.Event != consts.EmsTemplateCode {
|
||||
if models.Status == consts.EmsStatusUsed {
|
||||
return gerror.New("验证码已使用,请重新发送!")
|
||||
}
|
||||
}
|
||||
|
||||
if gtime.Now().After(models.CreatedAt.Add(time.Second * time.Duration(config.CodeExpire))) {
|
||||
return gerror.New("验证码已过期,请重新发送")
|
||||
}
|
||||
|
||||
if models.Code != in.Code {
|
||||
_, err = dao.SysEmsLog.Ctx(ctx).Where("id", models.Id).Increment("times", 1)
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
return gerror.New("验证码错误!")
|
||||
}
|
||||
|
||||
_, err = dao.SysEmsLog.Ctx(ctx).Where("id", models.Id).Data(g.Map{
|
||||
"times": models.Times + 1,
|
||||
"status": consts.EmsStatusUsed,
|
||||
"updated_at": gtime.Now(),
|
||||
}).Update()
|
||||
if err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@@ -20,6 +20,7 @@ import (
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/hgorm/hook"
|
||||
"hotgo/internal/library/location"
|
||||
"hotgo/internal/library/queue"
|
||||
"hotgo/internal/model/entity"
|
||||
@@ -54,7 +55,7 @@ func (s *sSysLog) Export(ctx context.Context, in sysin.LogListInp) (err error) {
|
||||
ReqId string `json:"req_id" description:"对外id"`
|
||||
TakeUpTime int64 `json:"take_up_time" description:"请求耗时"`
|
||||
CreatedAt *gtime.Time `json:"created_at" description:"创建时间"`
|
||||
MemberName string `json:"member_name"`
|
||||
MemberName string `json:"memberName"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
@@ -222,7 +223,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
|
||||
// View 获取指定字典类型信息
|
||||
func (s *sSysLog) View(ctx context.Context, in sysin.LogViewInp) (res *sysin.LogViewModel, err error) {
|
||||
|
||||
if err = dao.SysLog.Ctx(ctx).Where("id", in.Id).Scan(&res); err != nil {
|
||||
if err = dao.SysLog.Ctx(ctx).Hook(hook.CityLabel).Where("id", in.Id).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -13,12 +13,16 @@ import (
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/hgorm"
|
||||
"hotgo/internal/library/hgorm/handler"
|
||||
"hotgo/internal/library/hgorm/hook"
|
||||
"hotgo/internal/library/location"
|
||||
"hotgo/internal/library/queue"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/form"
|
||||
@@ -89,18 +93,19 @@ func (s *sSysLoginLog) List(ctx context.Context, in sysin.LoginLogListInp) (list
|
||||
{Dao: dao.SysLog, Alias: "sysLog"},
|
||||
})
|
||||
|
||||
if err = mod.Fields(fields).Handler(hgorm.HandlerFilterAuth).Page(in.Page, in.PerPage).OrderDesc(dao.SysLoginLog.Columns().Id).Scan(&list); err != nil {
|
||||
if err = mod.Fields(fields).Hook(hook.CityLabel).Handler(handler.FilterAuth).Page(in.Page, in.PerPage).OrderDesc(dao.SysLoginLog.Columns().Id).Scan(&list); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return list, totalCount, err
|
||||
}
|
||||
|
||||
for _, v := range list {
|
||||
// 获取省市编码对应的地区名称
|
||||
region, err := dao.SysProvinces.GetRegion(ctx, v.SysLogProvinceId, v.SysLogCityId)
|
||||
if err != nil {
|
||||
return list, totalCount, err
|
||||
}
|
||||
v.Region = region
|
||||
g.DumpWithType(v)
|
||||
//// 获取省市编码对应的地区名称
|
||||
//region, err := location.ParseRegion(ctx, v.SysLogProvinceId, v.SysLogCityId, 0)
|
||||
//if err != nil {
|
||||
// return list, totalCount, err
|
||||
//}
|
||||
//v.Region = region
|
||||
v.Os = useragent.GetOs(v.SysLogUserAgent)
|
||||
v.Browser = useragent.GetBrowser(v.SysLogUserAgent)
|
||||
}
|
||||
@@ -162,9 +167,10 @@ func (s *sSysLoginLog) View(ctx context.Context, in sysin.LoginLogViewInp) (res
|
||||
func (s *sSysLoginLog) Push(ctx context.Context, in sysin.LoginLogPushInp) {
|
||||
var models entity.SysLoginLog
|
||||
models.ReqId = gctx.CtxId(ctx)
|
||||
models.MemberId = in.Response.UserId
|
||||
models.MemberId = in.Response.Id
|
||||
models.Username = in.Input.Username
|
||||
models.LoginAt = gtime.Now()
|
||||
models.LoginIp = location.GetClientIp(ghttp.RequestFromCtx(ctx))
|
||||
models.Status = consts.StatusEnabled
|
||||
|
||||
if in.Err != nil {
|
||||
@@ -173,7 +179,7 @@ func (s *sSysLoginLog) Push(ctx context.Context, in sysin.LoginLogPushInp) {
|
||||
}
|
||||
|
||||
models.Response = gjson.New(consts.NilJsonToString)
|
||||
if in.Response != nil && in.Response.UserId > 0 {
|
||||
if in.Response != nil && in.Response.Id > 0 {
|
||||
models.Response = gjson.New(in.Response)
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ import (
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/hgorm"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"hotgo/utility/tree"
|
||||
@@ -165,7 +166,7 @@ func (s *sSysProvinces) MaxSort(ctx context.Context, in sysin.ProvincesMaxSortIn
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Sort = res.Sort + g.Cfg().MustGet(ctx, "hotgo.admin.maxSortIncrement").Int()
|
||||
res.Sort = form.DefaultMaxSort(ctx, res.Sort)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -258,3 +259,34 @@ func (s *sSysProvinces) UniqueId(ctx context.Context, in sysin.ProvincesUniqueId
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Select 省市区选项
|
||||
func (s *sSysProvinces) Select(ctx context.Context, in sysin.ProvincesSelectInp) (res *sysin.ProvincesSelectModel, err error) {
|
||||
res = new(sysin.ProvincesSelectModel)
|
||||
mod := dao.SysProvinces.Ctx(ctx).
|
||||
Fields("id as value, title as label, level").
|
||||
Where("pid", in.Value)
|
||||
|
||||
if err = mod.Order("sort asc,id asc").Scan(&res.List); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range res.List {
|
||||
if in.DataType == "p" {
|
||||
v.IsLeaf = true
|
||||
continue
|
||||
}
|
||||
if in.DataType == "pc" && v.Level >= 2 {
|
||||
v.IsLeaf = true
|
||||
continue
|
||||
}
|
||||
|
||||
if in.DataType == "pca" && v.Level >= 3 {
|
||||
v.IsLeaf = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import (
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/hgorm"
|
||||
"hotgo/internal/library/hgorm/handler"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
@@ -83,7 +84,7 @@ func (s *sSysServeLog) List(ctx context.Context, in sysin.ServeLogListInp) (list
|
||||
fields, err := hgorm.GenJoinSelect(ctx, sysin.ServeLogListModel{}, dao.SysServeLog, []*hgorm.Join{
|
||||
{Dao: dao.SysLog, Alias: "sysLog"},
|
||||
})
|
||||
if err = mod.Fields(fields).Handler(hgorm.HandlerFilterAuth).Page(in.Page, in.PerPage).OrderDesc(dao.SysServeLog.Columns().Id).Scan(&list); err != nil {
|
||||
if err = mod.Fields(fields).Handler(handler.FilterAuth).Page(in.Page, in.PerPage).OrderDesc(dao.SysServeLog.Columns().Id).Scan(&list); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return list, totalCount, err
|
||||
}
|
||||
|
@@ -143,7 +143,7 @@ func (s *sSysSmsLog) List(ctx context.Context, in sysin.SmsLogListInp) (list []*
|
||||
return list, totalCount, nil
|
||||
}
|
||||
|
||||
if err = mod.Page(int(in.Page), int(in.PerPage)).Order("id desc").Scan(&list); err != nil {
|
||||
if err = mod.Page(in.Page, in.PerPage).Order("id desc").Scan(&list); err != nil {
|
||||
err = gerror.Wrap(err, consts.ErrorORM)
|
||||
return list, totalCount, err
|
||||
}
|
||||
|
Reference in New Issue
Block a user