feat: add Go backend

This commit is contained in:
richjohnrambo
2026-10-10 20:56:17 +08:00
parent 50fcfd2d3d
commit f6024f0964
1040 changed files with 107169 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/text/gstr"
"hotgo/internal/consts"
"hotgo/internal/library/addons"
"hotgo/internal/library/dict"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
type sSysAddons struct{}
func NewSysAddons() *sSysAddons {
return &sSysAddons{}
}
func init() {
service.RegisterSysAddons(NewSysAddons())
}
// List 获取列表
func (s *sSysAddons) List(ctx context.Context, in *sysin.AddonsListInp) (list []*sysin.AddonsListModel, totalCount int, err error) {
sks := addons.GetSkeletons()
if len(sks) == 0 {
return
}
var (
i int
_, perPage, offset = form.CalPage(in.Page, in.PerPage)
)
for k, skeleton := range sks {
ok := k >= offset && i <= perPage
if !ok {
break
}
row := new(sysin.AddonsListModel)
row.Skeleton = *skeleton
if in.Group > 0 {
if row.Skeleton.Group != in.Group {
continue
}
}
if in.Name != "" {
if row.Skeleton.Label != in.Name && row.Skeleton.Name != in.Name {
continue
}
}
install, err := addons.ScanInstall(row.Skeleton.GetModule())
if err != nil {
continue
}
if install == nil {
row.InstallStatus = consts.AddonsInstallStatusNo
row.InstallVersion = "v0.0.0"
} else {
row.InstallStatus = install.Status
row.InstallVersion = install.Version
row.CanSave = gstr.CompareVersion(row.Skeleton.Version, install.Version) > 0
}
if in.Status > 0 {
if row.InstallStatus != in.Status {
continue
}
}
if row.Skeleton.Logo == "" {
row.Skeleton.Logo = consts.AddonsGroupIconMap[row.Skeleton.Group]
}
row.GroupName = dict.GetOptionLabel(consts.AddonsGroupOptions, row.Skeleton.Group)
list = append(list, row)
i++
}
totalCount = len(sks)
return
}
// Build 提交生成
func (s *sSysAddons) Build(ctx context.Context, in *sysin.AddonsBuildInp) (err error) {
config, err := service.SysConfig().GetLoadGenerate(ctx)
if err != nil {
return
}
if config == nil || config.Addon == nil {
err = gerror.New("没有找到有效的生成或插件配置,请检查配置文件是否正常")
return
}
option := new(addons.BuildOption)
option.Config = config.Addon
option.Skeleton = in.Skeleton
option.Extend = in.Extend
return addons.Build(ctx, option)
}
// Install 安装模块
func (s *sSysAddons) Install(ctx context.Context, in *sysin.AddonsInstallInp) (err error) {
return addons.Install(in.GetModule())
}
// Upgrade 更新模块
func (s *sSysAddons) Upgrade(ctx context.Context, in *sysin.AddonsUpgradeInp) (err error) {
return addons.Upgrade(in.GetModule())
}
// UnInstall 卸载模块
func (s *sSysAddons) UnInstall(ctx context.Context, in *sysin.AddonsUnInstallInp) (err error) {
return addons.UnInstall(in.GetModule())
}
+143
View File
@@ -0,0 +1,143 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/simple"
"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/gtime"
"github.com/gogf/gf/v2/text/gstr"
)
var AddonsMaskDemoField []string
type sSysAddonsConfig struct{}
func NewSysAddonsConfig() *sSysAddonsConfig {
return &sSysAddonsConfig{}
}
func init() {
service.RegisterSysAddonsConfig(NewSysAddonsConfig())
}
// GetConfigByGroup 获取指定分组的配置
func (s *sSysAddonsConfig) GetConfigByGroup(ctx context.Context, in *sysin.GetAddonsConfigInp) (res *sysin.GetAddonsConfigModel, err error) {
if in.AddonName == "" {
err = gerror.New("插件名称不能为空")
return
}
if in.Group == "" {
err = gerror.New("分组不能为空")
return
}
var (
mod = dao.SysAddonsConfig.Ctx(ctx)
models []*entity.SysAddonsConfig
cols = dao.SysAddonsConfig.Columns()
)
if err = mod.Fields(cols.Key, cols.Value, cols.Type).
Where(cols.AddonName, in.AddonName).
Where(cols.Group, in.Group).
Scan(&models); err != nil {
return nil, err
}
if len(models) > 0 {
res = new(sysin.GetAddonsConfigModel)
res.List = make(g.Map, len(models))
for _, v := range models {
val, err := s.ConversionType(ctx, v)
if err != nil {
return nil, err
}
res.List[v.Key] = val
if simple.IsDemo(ctx) && gstr.InArray(AddonsMaskDemoField, v.Key) {
res.List[v.Key] = consts.DemoTips
res.List[v.Key] = consts.DemoTips
}
}
}
return
}
// ConversionType 转换类型
func (s *sSysAddonsConfig) ConversionType(ctx context.Context, models *entity.SysAddonsConfig) (value interface{}, err error) {
if models == nil {
err = gerror.New("数据不存在")
return
}
return consts.ConvType(models.Value, models.Type), nil
}
// UpdateConfigByGroup 更新指定分组的配置
func (s *sSysAddonsConfig) UpdateConfigByGroup(ctx context.Context, in *sysin.UpdateAddonsConfigInp) (err error) {
if in.AddonName == "" {
err = gerror.New("插件名称不能为空")
return
}
if in.Group == "" {
err = gerror.New("分组不能为空")
return
}
var (
mod = dao.SysAddonsConfig.Ctx(ctx)
models []*entity.SysAddonsConfig
)
if err = mod.
Where("addon_name", in.AddonName).
Where("group", in.Group).
Scan(&models); err != nil {
return
}
err = dao.SysAddonsConfig.Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
for k, v := range in.List {
row := s.getConfigByKey(k, models)
// 新增
if row == nil {
return gerror.Newf("暂不支持从前台添加变量,请先在数据库表[%v]中配置变量:%v", dao.SysAddonsConfig.Table(), k)
}
// 更新
if _, err = dao.SysAddonsConfig.Ctx(ctx).Where("id", row.Id).Data(g.Map{"value": v, "updated_at": gtime.Now()}).Update(); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
}
return
})
return
}
func (s *sSysAddonsConfig) getConfigByKey(key string, models []*entity.SysAddonsConfig) *entity.SysAddonsConfig {
if len(models) == 0 {
return nil
}
for _, v := range models {
if key == v.Key {
return v
}
}
return nil
}
+737
View File
@@ -0,0 +1,737 @@
package sys
import (
"context"
"fmt"
"strings"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/encrypt"
"github.com/gogf/gf/v2/database/gdb"
"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/os/gtime"
"github.com/gogf/gf/v2/util/grand"
"golang.org/x/crypto/bcrypt"
)
type sSysAppMember struct{}
func NewSysAppMember() *sSysAppMember { return &sSysAppMember{} }
func init() { service.RegisterSysAppMember(NewSysAppMember()) }
func (s *sSysAppMember) memberModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysAppMember) profileModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.AppMemberProfile.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysAppMember) paymentModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.AppMemberPaymentAccount.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysAppMember) addressModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.AppMemberAddress.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysAppMember) walletModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysAppMember) adjustmentModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.WalletAdjustmentRequest.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysAppMember) noticeModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.AppMemberNotice.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func col(alias, column string) string { return alias + "." + column }
func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (list []*sysin.AppMemberListModel, totalCount int, err error) {
mc, pc, wc := dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns()
mod := s.memberModel(ctx).As("m").
LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("m", mc.Id)).
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").
LeftJoin(dao.AppMember.Table()+" parent", col("parent", mc.Id)+"="+col("m", mc.ParentId)).
Fields(
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.MobileMask),
col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)),
fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), col("m", mc.ParentId),
fmt.Sprintf("COALESCE(%s,'') parent_username", col("parent", mc.Username)), col("m", mc.InviteCode),
col("m", mc.MemberStatus), col("m", mc.RiskStatus), col("m", mc.AutoDispatch), col("m", mc.IsTestAccount),
fmt.Sprintf("CASE WHEN %s=1 THEN %d ELSE %d END member_category", col("m", mc.IsTestAccount), consts.AppMemberCategoryFake, consts.AppMemberCategoryReal),
col("m", mc.LastLoginAt), col("m", mc.LastLoginIpMask), col("m", mc.LastLoginRegion), col("m", mc.CreatedAt),
)
if in.Username != "" {
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, e := encrypt.HashField(ctx, in.Mobile)
if e != nil {
return nil, 0, e
}
mod = mod.Where(col("m", mc.MobileHash), hash)
}
if in.MemberStatus > 0 {
mod = mod.Where(col("m", mc.MemberStatus), in.MemberStatus)
}
if in.AutoDispatch > 0 {
mod = mod.Where(col("m", mc.AutoDispatch), in.AutoDispatch)
}
if in.MemberCategory == consts.AppMemberCategoryReal {
mod = mod.Where(col("m", mc.IsTestAccount), 2)
} else if in.MemberCategory == consts.AppMemberCategoryFake {
mod = mod.Where(col("m", mc.IsTestAccount), 1)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(col("m", mc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1])
}
err = mod.Page(in.Page, in.PerPage).OrderDesc(col("m", mc.Id)).ScanAndCount(&list, &totalCount, false)
if err != nil {
return nil, 0, gerror.Wrap(err, "获取会员列表失败")
}
return
}
func (s *sSysAppMember) View(ctx context.Context, in *sysin.AppMemberViewInp) (res *sysin.AppMemberViewModel, err error) {
mc, pc, wc := dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns()
res = new(sysin.AppMemberViewModel)
err = s.memberModel(ctx).As("m").LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("m", mc.Id)).LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").LeftJoin(dao.AppMember.Table()+" parent", col("parent", mc.Id)+"="+col("m", mc.ParentId)).Fields(
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), col("p", pc.IdNoMask), col("p", pc.KycStatus), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), col("m", mc.ParentId), fmt.Sprintf("COALESCE(%s,'') parent_username", col("parent", mc.Username)), col("m", mc.InviteCode), col("m", mc.MemberStatus), col("m", mc.RiskStatus), col("m", mc.AutoDispatch), col("m", mc.IsTestAccount), fmt.Sprintf("CASE WHEN %s=1 THEN %d ELSE %d END member_category", col("m", mc.IsTestAccount), consts.AppMemberCategoryFake, consts.AppMemberCategoryReal), col("m", mc.LastLoginAt), col("m", mc.LastLoginIpMask), col("m", mc.LastLoginRegion), col("m", mc.CreatedAt), col("m", mc.LevelId), col("m", mc.Level), col("m", mc.CreditScore),
).Where(col("m", mc.Id), in.Id).Scan(res)
if err != nil {
return nil, gerror.Wrap(err, "获取会员详情失败")
}
if res.Id == 0 {
return nil, gerror.New("会员不存在")
}
res.PaymentAccountCount, _ = s.paymentModel(ctx).Where(dao.AppMemberPaymentAccount.Columns().MemberId, in.Id).Count()
res.AddressCount, _ = s.addressModel(ctx).Where(dao.AppMemberAddress.Columns().MemberId, in.Id).Count()
token := "tr_" + fmt.Sprint(in.Id) + " "
res.TeamCount, _ = s.memberModel(ctx).WhereOr(dao.AppMember.Columns().ParentId, in.Id).WhereOrLike(dao.AppMember.Columns().Tree, "%"+token+"%").Count()
return
}
func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) error {
isTestAccount := 2
if in.MemberCategory == consts.AppMemberCategoryFake {
isTestAccount = 1
}
if isTestAccount == 1 && g.Cfg().MustGet(ctx, "system.mode").String() == "product" {
return gerror.New("生产环境禁止创建测试账号")
}
mc := dao.AppMember.Columns()
userId := uint64(contexts.GetUserId(ctx))
var before *entity.AppMember
if in.Id > 0 {
if err := s.memberModel(ctx).WherePri(in.Id).Scan(&before); err != nil {
return gerror.Wrap(err, "读取会员失败")
}
if before == nil {
return gerror.New("会员不存在")
}
}
count, err := s.memberModel(ctx).Where(mc.Username, in.Username).WhereNot(mc.Id, in.Id).Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("用户名已存在")
}
data := g.Map{mc.Username: in.Username, mc.LevelId: in.LevelId, mc.ParentId: in.ParentId, mc.RiskStatus: in.RiskStatus, mc.CreditScore: in.CreditScore, mc.AutoDispatch: in.AutoDispatch, mc.IsTestAccount: isTestAccount, mc.UpdatedBy: userId}
if in.Mobile != "" {
cipher, e := encrypt.EncryptField(ctx, in.Mobile)
if e != nil {
return e
}
hash, e := encrypt.HashField(ctx, in.Mobile)
if e != nil {
return e
}
count, e = s.memberModel(ctx).Where(mc.MobileHash, hash).WhereNot(mc.Id, in.Id).Count()
if e != nil {
return e
}
if count > 0 {
return gerror.New("手机号已存在")
}
data[mc.MobileCipher] = cipher
data[mc.MobileHash] = hash
data[mc.MobileMask] = encrypt.MaskMobile(in.Mobile)
}
if in.Password != "" {
hashed, e := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if e != nil {
return gerror.Wrap(e, "生成密码摘要失败")
}
data[mc.PasswordHash] = string(hashed)
}
if in.TradePassword != "" {
hashed, e := bcrypt.GenerateFromPassword([]byte(in.TradePassword), bcrypt.DefaultCost)
if e != nil {
return gerror.Wrap(e, "生成交易密码摘要失败")
}
data[mc.TradePasswordHash] = string(hashed)
}
level, tree := 1, ""
if in.ParentId > 0 {
var parent *entity.AppMember
if err = s.memberModel(ctx).WherePri(in.ParentId).Scan(&parent); err != nil {
return err
}
if parent == nil {
return gerror.New("上级会员不存在")
}
if parent.Id == in.Id {
return gerror.New("上级会员不能是自己")
}
level = parent.Level + 1
tree = parent.Tree + "tr_" + fmt.Sprint(parent.Id) + " "
}
data[mc.Level] = level
data[mc.Tree] = tree
if in.Id > 0 {
_, err = s.memberModel(ctx).WherePri(in.Id).Data(data).Update()
if err != nil {
return gerror.Wrap(err, "更新会员失败")
}
} else {
data[mc.MemberNo] = "MB" + gtime.Now().Format("YmdHis") + grand.S(5)
data[mc.InviteCode] = strings.ToUpper(grand.S(8))
data[mc.MemberStatus] = consts.AppMemberStatusNormal
data[mc.CreatedBy] = userId
result, e := s.memberModel(ctx).Fields(mc.MemberNo, mc.Username, mc.MobileCipher, mc.MobileHash, mc.MobileMask, mc.PasswordHash, mc.TradePasswordHash, mc.LevelId, mc.ParentId, mc.Level, mc.Tree, mc.InviteCode, mc.MemberStatus, mc.RiskStatus, mc.AutoDispatch, mc.CreditScore, mc.IsTestAccount, mc.CreatedBy, mc.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
if e != nil {
return gerror.Wrap(e, "创建会员失败")
}
id, _ := result.LastInsertId()
in.Id = uint64(id)
_, e = s.walletModel(ctx).Fields(dao.WalletAccount.Columns().MemberId, dao.WalletAccount.Columns().Currency).Hook(hook.SaveTenant).Data(g.Map{dao.WalletAccount.Columns().MemberId: in.Id, dao.WalletAccount.Columns().Currency: "CNY"}).Insert()
if e != nil {
return gerror.Wrap(e, "初始化会员钱包失败")
}
}
if in.RealName != "" || in.Id > 0 {
if err = s.saveProfile(ctx, in); err != nil {
return err
}
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "member.edit", TargetType: "app_member", TargetId: in.Id, Before: before, After: g.Map{"username": in.Username, "mobileChanged": in.Mobile != "", "parentId": in.ParentId, "riskStatus": in.RiskStatus, "autoDispatch": in.AutoDispatch, "memberCategory": in.MemberCategory}})
}
func (s *sSysAppMember) saveProfile(ctx context.Context, in *sysin.AppMemberEditInp) error {
pc := dao.AppMemberProfile.Columns()
var old *entity.AppMemberProfile
_ = s.profileModel(ctx).Where(pc.MemberId, in.Id).Scan(&old)
data := g.Map{pc.UpdatedBy: contexts.GetUserId(ctx)}
if in.RealName != "" {
v, e := encrypt.EncryptField(ctx, in.RealName)
if e != nil {
return e
}
data[pc.RealNameCipher] = v
data[pc.RealNameMask] = encrypt.MaskName(in.RealName)
}
if in.IdNo != "" {
v, e := encrypt.EncryptField(ctx, in.IdNo)
if e != nil {
return e
}
h, e := encrypt.HashField(ctx, in.IdNo)
if e != nil {
return e
}
data[pc.IdNoCipher] = v
data[pc.IdNoHash] = h
data[pc.IdNoMask] = encrypt.MaskAccount(in.IdNo)
}
if old != nil {
_, err := s.profileModel(ctx).WherePri(old.Id).Data(data).Update()
return err
}
data[pc.MemberId] = in.Id
data[pc.CreatedBy] = contexts.GetUserId(ctx)
_, err := s.profileModel(ctx).Fields(pc.MemberId, pc.RealNameCipher, pc.RealNameMask, pc.IdNoCipher, pc.IdNoHash, pc.IdNoMask, pc.CreatedBy, pc.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
return err
}
func (s *sSysAppMember) Delete(ctx context.Context, in *sysin.AppMemberDeleteInp) error {
var wallet *entity.WalletAccount
_ = s.walletModel(ctx).Where(dao.WalletAccount.Columns().MemberId, in.Id).Scan(&wallet)
if wallet != nil && (wallet.AvailableAmount != 0 || wallet.FrozenAmount != 0) {
return gerror.New("会员钱包仍有余额或冻结金额,不能注销")
}
var before *entity.AppMember
if err := s.memberModel(ctx).WherePri(in.Id).Scan(&before); err != nil {
return err
}
if before == nil {
return gerror.New("会员不存在")
}
mc := dao.AppMember.Columns()
_, err := s.memberModel(ctx).WherePri(in.Id).Data(g.Map{mc.MemberStatus: consts.AppMemberStatusCancelled, mc.DeletedBy: contexts.GetUserId(ctx), mc.DeletedAt: gtime.Now(), mc.UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return gerror.Wrap(err, "注销会员失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "member.cancel", TargetType: "app_member", TargetId: in.Id, Before: before, After: g.Map{"status": consts.AppMemberStatusCancelled}, Reason: in.Reason})
}
func (s *sSysAppMember) AutoDispatch(ctx context.Context, in *sysin.AppMemberAutoDispatchInp) error {
mc := dao.AppMember.Columns()
_, err := s.memberModel(ctx).WherePri(in.Id).Data(g.Map{mc.AutoDispatch: in.AutoDispatch, mc.UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return gerror.Wrap(err, "更新自动派单失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "member.auto_dispatch", TargetType: "app_member", TargetId: in.Id, After: g.Map{"autoDispatch": in.AutoDispatch}})
}
func (s *sSysAppMember) Category(ctx context.Context, in *sysin.AppMemberCategoryInp) error {
isTestAccount := 2
if in.MemberCategory == consts.AppMemberCategoryFake {
isTestAccount = 1
}
mc := dao.AppMember.Columns()
result, err := s.memberModel(ctx).WherePri(in.Id).Data(g.Map{mc.IsTestAccount: isTestAccount, mc.UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return gerror.Wrap(err, "切换会员类别失败")
}
affected, _ := result.RowsAffected()
if affected == 0 {
return gerror.New("会员不存在或类别未发生变化")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "member.category", TargetType: "app_member", TargetId: in.Id, After: g.Map{"memberCategory": in.MemberCategory}})
}
func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaymentListInp) (list []*sysin.AppMemberPaymentModel, err error) {
c := dao.AppMemberPaymentAccount.Columns()
err = s.paymentModel(ctx).Fields(c.Id, c.MemberId, c.AccountType, c.BankCode, c.BankName, c.HolderMask, c.MobileMask, c.AccountNoMask, c.QrAttachmentId, c.VerifyStatus, c.IsDefault, c.CreatedAt, c.UpdatedAt).Where(c.MemberId, in.MemberId).OrderDesc(c.IsDefault).OrderDesc(c.Id).Scan(&list)
if err != nil {
err = gerror.Wrap(err, "获取收款账户失败")
}
return
}
func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaymentEditInp) error {
c := dao.AppMemberPaymentAccount.Columns()
data := g.Map{c.MemberId: in.MemberId, c.AccountType: in.AccountType, c.BankCode: in.BankCode, c.BankName: in.BankName, c.QrAttachmentId: in.QrAttachmentId, c.VerifyStatus: in.VerifyStatus, c.IsDefault: in.IsDefault, c.UpdatedBy: contexts.GetUserId(ctx)}
if in.Holder != "" {
v, e := encrypt.EncryptField(ctx, in.Holder)
if e != nil {
return e
}
data[c.HolderCipher] = v
data[c.HolderMask] = encrypt.MaskName(in.Holder)
}
if in.Mobile != "" {
v, e := encrypt.EncryptField(ctx, in.Mobile)
if e != nil {
return e
}
data[c.MobileCipher] = v
data[c.MobileMask] = encrypt.MaskMobile(in.Mobile)
}
if in.AccountNo != "" {
v, e := encrypt.EncryptField(ctx, in.AccountNo)
if e != nil {
return e
}
h, e := encrypt.HashField(ctx, in.AccountNo)
if e != nil {
return e
}
data[c.AccountNoCipher] = v
data[c.AccountNoHash] = h
data[c.AccountNoMask] = encrypt.MaskAccount(in.AccountNo)
}
if in.IsDefault == 1 {
_, _ = s.paymentModel(ctx).Where(c.MemberId, in.MemberId).Data(g.Map{c.IsDefault: 2}).Update()
}
if in.Id > 0 {
_, err := s.paymentModel(ctx).WherePri(in.Id).Where(c.MemberId, in.MemberId).Data(data).Update()
if err != nil {
return gerror.Wrap(err, "更新收款账户失败")
}
} else {
data[c.CreatedBy] = contexts.GetUserId(ctx)
_, err := s.paymentModel(ctx).Fields(c.MemberId, c.AccountType, c.BankCode, c.BankName, c.HolderCipher, c.HolderMask, c.MobileCipher, c.MobileMask, c.AccountNoCipher, c.AccountNoHash, c.AccountNoMask, c.QrAttachmentId, c.VerifyStatus, c.IsDefault, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
if err != nil {
return gerror.Wrap(err, "创建收款账户失败")
}
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "payment.edit", TargetType: "app_member_payment_account", TargetId: in.Id, After: g.Map{"memberId": in.MemberId, "accountType": in.AccountType, "bankName": in.BankName, "sensitiveChanged": in.Holder != "" || in.Mobile != "" || in.AccountNo != ""}})
}
func (s *sSysAppMember) PaymentDelete(ctx context.Context, in *sysin.AppMemberPaymentDeleteInp) error {
c := dao.AppMemberPaymentAccount.Columns()
_, err := s.paymentModel(ctx).WherePri(in.Id).Data(g.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return gerror.Wrap(err, "删除收款账户失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "payment.delete", TargetType: "app_member_payment_account", TargetId: in.Id})
}
func (s *sSysAppMember) AddressList(ctx context.Context, in *sysin.AppMemberAddressListInp) (list []*sysin.AppMemberAddressModel, err error) {
c := dao.AppMemberAddress.Columns()
err = s.addressModel(ctx).Fields(c.Id, c.MemberId, c.ReceiverMask, c.MobileMask, c.Province, c.City, c.District, c.AddressMask, c.IsDefault, c.CreatedAt, c.UpdatedAt).Where(c.MemberId, in.MemberId).OrderDesc(c.IsDefault).OrderDesc(c.Id).Scan(&list)
if err != nil {
err = gerror.Wrap(err, "获取会员地址失败")
}
return
}
func (s *sSysAppMember) AddressEdit(ctx context.Context, in *sysin.AppMemberAddressEditInp) error {
c := dao.AppMemberAddress.Columns()
data := g.Map{c.MemberId: in.MemberId, c.Province: in.Province, c.City: in.City, c.District: in.District, c.IsDefault: in.IsDefault, c.UpdatedBy: contexts.GetUserId(ctx)}
if in.Receiver != "" {
v, e := encrypt.EncryptField(ctx, in.Receiver)
if e != nil {
return e
}
data[c.ReceiverCipher] = v
data[c.ReceiverMask] = encrypt.MaskName(in.Receiver)
}
if in.Mobile != "" {
v, e := encrypt.EncryptField(ctx, in.Mobile)
if e != nil {
return e
}
data[c.MobileCipher] = v
data[c.MobileMask] = encrypt.MaskMobile(in.Mobile)
}
if in.Address != "" {
v, e := encrypt.EncryptField(ctx, in.Address)
if e != nil {
return e
}
data[c.AddressCipher] = v
data[c.AddressMask] = encrypt.MaskAddress(in.Address)
}
if in.IsDefault == 1 {
_, _ = s.addressModel(ctx).Where(c.MemberId, in.MemberId).Data(g.Map{c.IsDefault: 2}).Update()
}
if in.Id > 0 {
_, err := s.addressModel(ctx).WherePri(in.Id).Where(c.MemberId, in.MemberId).Data(data).Update()
if err != nil {
return gerror.Wrap(err, "更新地址失败")
}
} else {
data[c.CreatedBy] = contexts.GetUserId(ctx)
_, err := s.addressModel(ctx).Fields(c.MemberId, c.ReceiverCipher, c.ReceiverMask, c.MobileCipher, c.MobileMask, c.Province, c.City, c.District, c.AddressCipher, c.AddressMask, c.IsDefault, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
if err != nil {
return gerror.Wrap(err, "创建地址失败")
}
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "address.edit", TargetType: "app_member_address", TargetId: in.Id, After: g.Map{"memberId": in.MemberId, "region": in.Province + in.City + in.District, "sensitiveChanged": in.Receiver != "" || in.Mobile != "" || in.Address != ""}})
}
func (s *sSysAppMember) AddressDelete(ctx context.Context, in *sysin.AppMemberAddressDeleteInp) error {
c := dao.AppMemberAddress.Columns()
_, err := s.addressModel(ctx).WherePri(in.Id).Data(g.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return gerror.Wrap(err, "删除地址失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "address.delete", TargetType: "app_member_address", TargetId: in.Id})
}
func (s *sSysAppMember) Sensitive(ctx context.Context, in *sysin.AppMemberSensitiveInp) (res *sysin.AppMemberSensitiveModel, err error) {
res = &sysin.AppMemberSensitiveModel{Fields: map[string]string{}, AccessNo: "SA" + gtime.Now().Format("YmdHis") + grand.S(6)}
switch in.TargetType {
case "member":
var member *entity.AppMember
if err = s.memberModel(ctx).WherePri(in.TargetId).Scan(&member); err == nil && member != nil {
res.Fields["mobile"], err = encrypt.DecryptField(ctx, member.MobileCipher)
var p *entity.AppMemberProfile
_ = s.profileModel(ctx).Where(dao.AppMemberProfile.Columns().MemberId, in.TargetId).Scan(&p)
if p != nil {
res.Fields["realName"], _ = encrypt.DecryptField(ctx, p.RealNameCipher)
res.Fields["idNo"], _ = encrypt.DecryptField(ctx, p.IdNoCipher)
}
}
case "payment":
var row *entity.AppMemberPaymentAccount
if err = s.paymentModel(ctx).WherePri(in.TargetId).Scan(&row); err == nil && row != nil {
res.Fields["holder"], _ = encrypt.DecryptField(ctx, row.HolderCipher)
res.Fields["mobile"], _ = encrypt.DecryptField(ctx, row.MobileCipher)
res.Fields["accountNo"], err = encrypt.DecryptField(ctx, row.AccountNoCipher)
}
case "address":
var row *entity.AppMemberAddress
if err = s.addressModel(ctx).WherePri(in.TargetId).Scan(&row); err == nil && row != nil {
res.Fields["receiver"], _ = encrypt.DecryptField(ctx, row.ReceiverCipher)
res.Fields["mobile"], _ = encrypt.DecryptField(ctx, row.MobileCipher)
res.Fields["address"], err = encrypt.DecryptField(ctx, row.AddressCipher)
}
}
if err == nil && len(res.Fields) == 0 {
err = gerror.New("敏感信息目标不存在或无可查看字段")
}
result := 1
if err != nil {
result = 2
}
lc := dao.SensitiveAccessLog.Columns()
_, logErr := dao.SensitiveAccessLog.Ctx(ctx).Fields(lc.AccessNo, lc.ActorId, lc.TargetType, lc.TargetId, lc.FieldSet, lc.PurposeCode, lc.PurposeText, lc.AuthorizationNo, lc.MfaVerified, lc.Result).Hook(hook.SaveTenant).Data(g.Map{lc.AccessNo: res.AccessNo, lc.ActorId: contexts.GetUserId(ctx), lc.TargetType: in.TargetType, lc.TargetId: in.TargetId, lc.FieldSet: gjson.New(res.Fields), lc.PurposeCode: in.PurposeCode, lc.PurposeText: in.PurposeText, lc.AuthorizationNo: res.AccessNo, lc.MfaVerified: 0, lc.Result: result}).Insert()
if logErr != nil {
return nil, gerror.Wrap(logErr, "记录敏感信息访问失败")
}
if err != nil {
return nil, err
}
return
}
func (s *sSysAppMember) Team(ctx context.Context, in *sysin.AppMemberTeamInp) (list []*sysin.AppMemberTeamModel, total int, err error) {
var root *entity.AppMember
if err = s.memberModel(ctx).WherePri(in.MemberId).Scan(&root); err != nil {
return
}
if root == nil {
return nil, 0, gerror.New("会员不存在")
}
mc, wc := dao.AppMember.Columns(), dao.WalletAccount.Columns()
token := "tr_" + fmt.Sprint(in.MemberId) + " "
maxAbsolute := root.Level + in.MaxLevel
mod := s.memberModel(ctx).As("m").LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").Fields(col("m", mc.Id), col("m", mc.Username), col("m", mc.MobileMask), col("m", mc.ParentId), fmt.Sprintf("%s-%d relative_level", col("m", mc.Level), root.Level), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), col("m", mc.MemberStatus), col("m", mc.CreatedAt)).WhereLike(col("m", mc.Tree), "%"+token+"%").WhereLTE(col("m", mc.Level), maxAbsolute)
err = mod.Page(in.Page, in.PerPage).OrderAsc(col("m", mc.Level)).OrderAsc(col("m", mc.Id)).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "查询会员团队失败")
}
return
}
func (s *sSysAppMember) StatusRequest(ctx context.Context, in *sysin.AppMemberStatusRequestInp) (res *sysin.AppMemberApprovalModel, err error) {
var member *entity.AppMember
if err = s.memberModel(ctx).WherePri(in.MemberId).Scan(&member); err != nil {
return
}
if member == nil {
return nil, gerror.New("会员不存在")
}
if member.MemberStatus == in.ToStatus {
return nil, gerror.New("会员已处于目标状态")
}
payload := gjson.New(g.Map{"memberId": in.MemberId, "fromStatus": member.MemberStatus, "toStatus": in.ToStatus, "reasonCode": in.ReasonCode, "reasonText": in.ReasonText, "expireAt": in.ExpireAt})
approval, err := service.SysApproval().Create(ctx, &sysin.ApprovalCreateInp{ApprovalType: "member_status", TargetType: "app_member", TargetId: in.MemberId, Action: "change_status", Title: "会员状态变更 " + member.Username, PayloadJson: payload, DiffJson: gjson.New(g.Map{"memberStatus": g.Map{"before": member.MemberStatus, "after": in.ToStatus}}), RiskLevel: consts.ApprovalRiskHigh, RequiredApprovals: 2, Remark: in.ReasonText})
if err != nil {
return nil, err
}
if err = service.SysApproval().Submit(ctx, &sysin.ApprovalSubmitInp{Id: approval.Id, Version: 0}); err != nil {
return nil, err
}
return &sysin.AppMemberApprovalModel{ApprovalId: approval.Id, ApprovalNo: approval.ApprovalNo}, nil
}
func (s *sSysAppMember) StatusExecute(ctx context.Context, in *sysin.AppMemberStatusExecuteInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var approval *entity.ApprovalRequest
if err := service.SysApproval().Model(ctx).WherePri(in.ApprovalId).LockUpdate().Scan(&approval); err != nil {
return err
}
if approval == nil || approval.ApprovalType != "member_status" {
return gerror.New("会员状态审批不存在")
}
if approval.ApprovalStatus != consts.ApprovalStatusApproved {
return gerror.New("审批尚未通过")
}
if approval.ExecutedAt != nil {
return gerror.New("该审批已执行")
}
memberId := approval.PayloadJson.Get("memberId").Uint64()
toStatus := approval.PayloadJson.Get("toStatus").Int()
reasonCode := approval.PayloadJson.Get("reasonCode").String()
reasonText := approval.PayloadJson.Get("reasonText").String()
var member *entity.AppMember
if err := s.memberModel(ctx).WherePri(memberId).LockUpdate().Scan(&member); err != nil {
return err
}
if member == nil {
return gerror.New("会员不存在")
}
mc := dao.AppMember.Columns()
if _, err := s.memberModel(ctx).WherePri(memberId).Data(g.Map{mc.MemberStatus: toStatus, mc.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return gerror.Wrap(err, "更新会员状态失败")
}
sc := dao.AppMemberStatusLog.Columns()
_, err := dao.AppMemberStatusLog.Ctx(ctx).Fields(sc.MemberId, sc.FromStatus, sc.ToStatus, sc.ReasonCode, sc.ReasonText, sc.EffectiveAt, sc.ExpireAt, sc.ApprovalRequestId, sc.OperatorId).Hook(hook.SaveTenant).Data(g.Map{sc.MemberId: memberId, sc.FromStatus: member.MemberStatus, sc.ToStatus: toStatus, sc.ReasonCode: reasonCode, sc.ReasonText: reasonText, sc.EffectiveAt: gtime.Now(), sc.ExpireAt: approval.PayloadJson.Get("expireAt"), sc.ApprovalRequestId: approval.Id, sc.OperatorId: contexts.GetUserId(ctx)}).Insert()
if err != nil {
return gerror.Wrap(err, "记录会员状态变更失败")
}
_, err = service.SysApproval().Model(ctx).WherePri(approval.Id).Data(g.Map{dao.ApprovalRequest.Columns().ExecutedAt: gtime.Now(), dao.ApprovalRequest.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return err
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "member.status.execute", TargetType: "app_member", TargetId: memberId, Before: g.Map{"memberStatus": member.MemberStatus}, After: g.Map{"memberStatus": toStatus}, Reason: reasonText, ApprovalNo: approval.ApprovalNo})
})
}
func (s *sSysAppMember) WalletList(ctx context.Context, in *sysin.WalletAdjustmentListInp) (list []*sysin.WalletAdjustmentListModel, total int, err error) {
ac, mc := dao.WalletAdjustmentRequest.Columns(), dao.AppMember.Columns()
mod := s.adjustmentModel(ctx).As("a").LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("a", ac.MemberId)).Fields(col("a", ac.Id), col("a", ac.RequestNo), col("a", ac.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("a", ac.AdjustType), col("a", ac.Direction), col("a", ac.Amount), col("a", ac.ReasonCode), col("a", ac.ReasonText), col("a", ac.RequestStatus), col("a", ac.ApprovalRequestId), col("a", ac.LedgerId), col("a", ac.CreatedAt))
if in.MemberId > 0 {
mod = mod.Where(col("a", ac.MemberId), in.MemberId)
}
if in.RequestStatus > 0 {
mod = mod.Where(col("a", ac.RequestStatus), in.RequestStatus)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(col("a", ac.CreatedAt), in.CreatedAt[0], in.CreatedAt[1])
}
err = mod.Page(in.Page, in.PerPage).OrderDesc(col("a", ac.Id)).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取资金调账列表失败")
}
return
}
func (s *sSysAppMember) LedgerList(ctx context.Context, in *sysin.WalletLedgerListInp) (list []*sysin.WalletLedgerListModel, total int, err error) {
c := dao.WalletLedger.Columns()
mod := handler.Model(dao.WalletLedger.Ctx(ctx), &handler.Option{FilterTenant: true}).
Fields(c.Id, c.LedgerNo, c.BizType, c.BizNo, c.EntryType, c.Direction, c.Amount, c.BeforeAmount, c.AfterAmount, c.Currency, c.Remark, c.OccurredAt).
Where(c.MemberId, in.MemberId)
err = mod.Page(in.Page, in.PerPage).OrderDesc(c.OccurredAt).OrderDesc(c.Id).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取会员账变流水失败")
}
return
}
func (s *sSysAppMember) WalletRequest(ctx context.Context, in *sysin.WalletAdjustmentRequestInp) (res *sysin.AppMemberApprovalModel, err error) {
var member *entity.AppMember
if err = s.memberModel(ctx).WherePri(in.MemberId).Scan(&member); err != nil {
return
}
if member == nil {
return nil, gerror.New("会员不存在")
}
wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount
if err = s.walletModel(ctx).Where(wc.MemberId, in.MemberId).Where(wc.Currency, "CNY").Scan(&account); err != nil {
return
}
if account == nil {
return nil, gerror.New("会员钱包不存在")
}
payload := gjson.New(g.Map{"memberId": in.MemberId, "accountId": account.Id, "adjustType": in.AdjustType, "direction": in.Direction, "amount": in.Amount, "reasonCode": in.ReasonCode, "reasonText": in.ReasonText, "idempotencyKey": in.IdempotencyKey, "evidence": in.EvidenceJson})
approval, err := service.SysApproval().Create(ctx, &sysin.ApprovalCreateInp{ApprovalType: "wallet_adjustment", TargetType: "wallet_account", TargetId: account.Id, Action: "adjust_balance", Title: "会员资金调账 " + member.Username, PayloadJson: payload, DiffJson: gjson.New(g.Map{"direction": in.Direction, "amount": in.Amount}), RiskLevel: consts.ApprovalRiskHigh, RequiredApprovals: 2, Remark: in.ReasonText})
if err != nil {
return nil, err
}
if err = service.SysApproval().Submit(ctx, &sysin.ApprovalSubmitInp{Id: approval.Id, Version: 0}); err != nil {
return nil, err
}
c := dao.WalletAdjustmentRequest.Columns()
_, err = s.adjustmentModel(ctx).Fields(c.RequestNo, c.MemberId, c.AccountId, c.AdjustType, c.Direction, c.Amount, c.ReasonCode, c.ReasonText, c.EvidenceJson, c.IdempotencyKey, c.RequestStatus, c.ApprovalRequestId, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.RequestNo: "WA" + gtime.Now().Format("YmdHis") + grand.S(6), c.MemberId: in.MemberId, c.AccountId: account.Id, c.AdjustType: in.AdjustType, c.Direction: in.Direction, c.Amount: in.Amount, c.ReasonCode: in.ReasonCode, c.ReasonText: in.ReasonText, c.EvidenceJson: in.EvidenceJson, c.IdempotencyKey: in.IdempotencyKey, c.RequestStatus: consts.WalletRequestPending, c.ApprovalRequestId: approval.Id, c.CreatedBy: contexts.GetUserId(ctx), c.UpdatedBy: contexts.GetUserId(ctx)}).Insert()
if err != nil {
return nil, gerror.Wrap(err, "创建调账申请失败")
}
return &sysin.AppMemberApprovalModel{ApprovalId: approval.Id, ApprovalNo: approval.ApprovalNo}, nil
}
func (s *sSysAppMember) WalletExecute(ctx context.Context, in *sysin.WalletAdjustmentExecuteInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var request *entity.WalletAdjustmentRequest
if err := s.adjustmentModel(ctx).WherePri(in.Id).LockUpdate().Scan(&request); err != nil {
return err
}
if request == nil {
return gerror.New("调账申请不存在")
}
if request.RequestStatus == consts.WalletRequestExecuted {
return gerror.New("调账申请已执行")
}
var approval *entity.ApprovalRequest
if err := service.SysApproval().Model(ctx).WherePri(request.ApprovalRequestId).LockUpdate().Scan(&approval); err != nil {
return err
}
if approval == nil || approval.ApprovalStatus != consts.ApprovalStatusApproved {
return gerror.New("审批尚未通过")
}
wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount
if err := s.walletModel(ctx).WherePri(request.AccountId).LockUpdate().Scan(&account); err != nil {
return err
}
if account == nil {
return gerror.New("钱包账户不存在")
}
after := account.AvailableAmount + request.Amount
if request.Direction == consts.WalletDirectionExpense {
after = account.AvailableAmount - request.Amount
}
if after < 0 {
return gerror.New("会员可用余额不足,不能执行扣减")
}
lc := dao.WalletLedger.Columns()
ledgerNo := "WL" + gtime.Now().Format("YmdHis") + grand.S(7)
result, err := dao.WalletLedger.Ctx(ctx).Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt).Hook(hook.SaveTenant).Data(g.Map{lc.LedgerNo: ledgerNo, lc.AccountId: account.Id, lc.MemberId: request.MemberId, lc.BizType: "manual_adjustment", lc.BizNo: request.RequestNo, lc.EntryType: fmt.Sprint(request.AdjustType), lc.Direction: request.Direction, lc.Amount: request.Amount, lc.BeforeAmount: account.AvailableAmount, lc.AfterAmount: after, lc.Currency: account.Currency, lc.IdempotencyKey: request.IdempotencyKey, lc.Remark: request.ReasonText, lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: gtime.Now()}).Insert()
if err != nil {
return gerror.Wrap(err, "写入账变流水失败")
}
ledgerId, _ := result.LastInsertId()
walletData := g.Map{wc.AvailableAmount: after, wc.Version: gdb.Raw(wc.Version + "+1")}
if request.Direction == consts.WalletDirectionIncome {
walletData[wc.TotalIncome] = gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(request.Amount))
} else {
walletData[wc.TotalExpense] = gdb.Raw(wc.TotalExpense + "+" + fmt.Sprint(request.Amount))
}
update, err := s.walletModel(ctx).WherePri(account.Id).Where(wc.Version, account.Version).Data(walletData).Update()
if err != nil {
return gerror.Wrap(err, "更新钱包余额失败")
}
rows, _ := update.RowsAffected()
if rows != 1 {
return gerror.New("钱包余额已变化,请重试")
}
rc := dao.WalletAdjustmentRequest.Columns()
_, err = s.adjustmentModel(ctx).WherePri(request.Id).Data(g.Map{rc.RequestStatus: consts.WalletRequestExecuted, rc.LedgerId: ledgerId, rc.UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return err
}
_, err = service.SysApproval().Model(ctx).WherePri(approval.Id).Data(g.Map{dao.ApprovalRequest.Columns().ExecutedAt: gtime.Now(), dao.ApprovalRequest.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return err
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "wallet", Action: "wallet.adjust.execute", TargetType: "wallet_account", TargetId: account.Id, Before: g.Map{"availableAmount": account.AvailableAmount}, After: g.Map{"availableAmount": after, "ledgerId": ledgerId}, Reason: request.ReasonText, ApprovalNo: approval.ApprovalNo})
})
}
func (s *sSysAppMember) NoticeList(ctx context.Context, in *sysin.MemberNoticeListInp) (list []*sysin.MemberNoticeListModel, total int, err error) {
nc, mc := dao.AppMemberNotice.Columns(), dao.AppMember.Columns()
mod := s.noticeModel(ctx).As("n").LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("n", nc.MemberId)).Fields(col("n", nc.Id), col("n", nc.TenantId), col("n", nc.MemberId), col("n", nc.Title), col("n", nc.Content), col("n", nc.SourceType), col("n", nc.StartAt), col("n", nc.EndAt), col("n", nc.NoticeStatus), col("n", nc.CreatedBy), col("n", nc.UpdatedBy), col("n", nc.CreatedAt), col("n", nc.UpdatedAt), fmt.Sprintf("COALESCE(%s,'全体会员') username", col("m", mc.Username)))
if in.Title != "" {
mod = mod.WhereLike(col("n", nc.Title), "%"+in.Title+"%")
}
if in.SourceType > 0 {
mod = mod.Where(col("n", nc.SourceType), in.SourceType)
}
if in.NoticeStatus > 0 {
mod = mod.Where(col("n", nc.NoticeStatus), in.NoticeStatus)
}
err = mod.Page(in.Page, in.PerPage).OrderDesc(col("n", nc.Id)).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取会员通知失败")
}
return
}
func (s *sSysAppMember) NoticeEdit(ctx context.Context, in *sysin.MemberNoticeEditInp) error {
if in.SourceType == consts.MemberNoticeSimulation && g.Cfg().MustGet(ctx, "system.mode").String() == "product" {
return gerror.New("生产环境禁止创建运营模拟通知")
}
c := dao.AppMemberNotice.Columns()
_, err := s.noticeModel(ctx).Fields(c.MemberId, c.Title, c.Content, c.SourceType, c.StartAt, c.EndAt, c.NoticeStatus, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.MemberId: in.MemberId, c.Title: in.Title, c.Content: in.Content, c.SourceType: in.SourceType, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.NoticeStatus: consts.MemberNoticeActive, c.CreatedBy: contexts.GetUserId(ctx), c.UpdatedBy: contexts.GetUserId(ctx)}).Insert()
if err != nil {
return gerror.Wrap(err, "创建会员通知失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "notice.create", TargetType: "app_member_notice", After: g.Map{"memberId": in.MemberId, "title": in.Title, "sourceType": in.SourceType, "startAt": in.StartAt, "endAt": in.EndAt}})
}
func (s *sSysAppMember) NoticeRevoke(ctx context.Context, in *sysin.MemberNoticeRevokeInp) error {
c := dao.AppMemberNotice.Columns()
_, err := s.noticeModel(ctx).WherePri(in.Id).Data(g.Map{c.NoticeStatus: consts.MemberNoticeRevoked, c.UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return gerror.Wrap(err, "撤回会员通知失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member", Action: "notice.revoke", TargetType: "app_member_notice", TargetId: in.Id, Reason: in.Reason})
}
+352
View File
@@ -0,0 +1,352 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"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/os/gtime"
"github.com/gogf/gf/v2/util/grand"
)
type sSysApproval struct{}
func NewSysApproval() *sSysApproval { return &sSysApproval{} }
func init() { service.RegisterSysApproval(NewSysApproval()) }
func (s *sSysApproval) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{FilterTenant: true})
}
return handler.Model(dao.ApprovalRequest.Ctx(ctx), option...)
}
func (s *sSysApproval) List(ctx context.Context, in *sysin.ApprovalListInp) (list []*sysin.ApprovalListModel, totalCount int, err error) {
mod := s.Model(ctx).Fields(sysin.ApprovalListModel{})
columns := dao.ApprovalRequest.Columns()
if in.ApprovalNo != "" {
mod = mod.WhereLike(columns.ApprovalNo, "%"+in.ApprovalNo+"%")
}
if in.ApprovalType != "" {
mod = mod.Where(columns.ApprovalType, in.ApprovalType)
}
if in.TargetType != "" {
mod = mod.Where(columns.TargetType, in.TargetType)
}
if in.TargetId > 0 {
mod = mod.Where(columns.TargetId, in.TargetId)
}
if in.ApprovalStatus > 0 {
mod = mod.Where(columns.ApprovalStatus, in.ApprovalStatus)
}
if in.RiskLevel > 0 {
mod = mod.Where(columns.RiskLevel, in.RiskLevel)
}
if in.Applicant != "" {
ids, queryErr := service.AdminMember().GetIdsByKeyword(ctx, in.Applicant)
if queryErr != nil {
return nil, 0, queryErr
}
mod = mod.WhereIn(columns.ApplicantId, ids)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
mod = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取审批列表失败")
}
for _, item := range list {
item.AllowedActions = s.allowedActions(ctx, item.ApplicantId, item.ApprovalStatus)
}
return
}
func (s *sSysApproval) View(ctx context.Context, in *sysin.ApprovalViewInp) (res *sysin.ApprovalViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取审批详情失败")
}
if res == nil || res.Id == 0 {
return nil, gerror.New("审批记录不存在或无权查看")
}
if err = dao.ApprovalAction.Ctx(ctx).
Where(dao.ApprovalAction.Columns().ApprovalRequestId, in.Id).
OrderAsc(dao.ApprovalAction.Columns().StepNo).
OrderAsc(dao.ApprovalAction.Columns().Id).
Scan(&res.Actions); err != nil {
return nil, gerror.Wrap(err, "获取审批记录失败")
}
if res.Actions == nil {
res.Actions = []*sysin.ApprovalActionModel{}
}
res.AllowedActions = s.allowedActions(ctx, res.ApplicantId, res.ApprovalStatus)
return
}
func (s *sSysApproval) allowedActions(ctx context.Context, applicantId uint64, status int) []string {
userId := uint64(contexts.GetUserId(ctx))
actions := make([]string, 0, 2)
if userId == applicantId {
if status == consts.ApprovalStatusDraft {
actions = append(actions, "submit")
}
if status == consts.ApprovalStatusDraft || status == consts.ApprovalStatusPending || status == consts.ApprovalStatusReviewing {
actions = append(actions, "cancel")
}
return actions
}
if status == consts.ApprovalStatusPending || status == consts.ApprovalStatusReviewing {
actions = append(actions, "approve", "reject")
}
return actions
}
func (s *sSysApproval) Create(ctx context.Context, in *sysin.ApprovalCreateInp) (res *sysin.ApprovalCreateModel, err error) {
userId := contexts.GetUserId(ctx)
if userId <= 0 {
return nil, gerror.New("未获取到当前操作人")
}
approvalNo := "AP" + gtime.Now().Format("YmdHis") + grand.S(6)
data := &sysin.ApprovalRequestInsertFields{
ApprovalNo: approvalNo,
ApprovalType: in.ApprovalType,
TargetType: in.TargetType,
TargetId: in.TargetId,
Action: in.Action,
Title: in.Title,
PayloadJson: in.PayloadJson,
DiffJson: in.DiffJson,
RiskLevel: in.RiskLevel,
RequiredApprovals: in.RequiredApprovals,
ApprovalStatus: consts.ApprovalStatusDraft,
ApplicantId: uint64(userId),
ExpireAt: in.ExpireAt,
Version: 0,
Remark: in.Remark,
CreatedBy: uint64(userId),
}
result, err := s.Model(ctx, &handler.Option{}).
Fields(sysin.ApprovalRequestInsertFields{}).
Hook(hook.SaveTenant).
Data(data).Insert()
if err != nil {
return nil, gerror.Wrap(err, "创建审批草稿失败")
}
id, err := result.LastInsertId()
if err != nil {
return nil, gerror.Wrap(err, "获取审批ID失败")
}
return &sysin.ApprovalCreateModel{Id: uint64(id), ApprovalNo: approvalNo}, nil
}
func (s *sSysApproval) Submit(ctx context.Context, in *sysin.ApprovalSubmitInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
request, err := s.lockRequest(ctx, in.Id)
if err != nil {
return err
}
if request.ApplicantId != uint64(contexts.GetUserId(ctx)) {
return gerror.New("只能提交本人创建的审批")
}
if request.ApprovalStatus != consts.ApprovalStatusDraft {
return gerror.New("仅草稿状态可以提交")
}
if request.Version != in.Version {
return gerror.New("审批数据已更新,请刷新后重试")
}
now := gtime.Now()
if request.ExpireAt != nil && request.ExpireAt.Before(now) {
return gerror.New("审批已超过有效期,无法提交")
}
if err = s.updateStatus(ctx, request, consts.ApprovalStatusPending, now, false); err != nil {
return err
}
return s.appendAction(ctx, request, 1, consts.ApprovalActionSubmit, "", "提交审批", 0)
})
}
func (s *sSysApproval) Approve(ctx context.Context, in *sysin.ApprovalDecisionInp) error {
return s.decide(ctx, in, true)
}
func (s *sSysApproval) Reject(ctx context.Context, in *sysin.ApprovalDecisionInp) error {
if in.Comment == "" {
return gerror.New("驳回时必须填写审批意见")
}
return s.decide(ctx, in, false)
}
func (s *sSysApproval) Cancel(ctx context.Context, in *sysin.ApprovalCancelInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
request, err := s.lockRequest(ctx, in.Id)
if err != nil {
return err
}
if request.ApplicantId != uint64(contexts.GetUserId(ctx)) {
return gerror.New("只能取消本人发起的审批")
}
if request.Version != in.Version {
return gerror.New("审批数据已更新,请刷新后重试")
}
if request.ApprovalStatus != consts.ApprovalStatusDraft &&
request.ApprovalStatus != consts.ApprovalStatusPending &&
request.ApprovalStatus != consts.ApprovalStatusReviewing {
return gerror.New("当前审批状态不允许取消")
}
stepNo, err := s.nextStep(ctx, request.Id)
if err != nil {
return err
}
if err = s.updateStatus(ctx, request, consts.ApprovalStatusCancelled, gtime.Now(), true); err != nil {
return err
}
return s.appendAction(ctx, request, stepNo, consts.ApprovalActionCancel, "", in.Comment, 0)
})
}
func (s *sSysApproval) decide(ctx context.Context, in *sysin.ApprovalDecisionInp, approved bool) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
request, err := s.lockRequest(ctx, in.Id)
if err != nil {
return err
}
actorId := uint64(contexts.GetUserId(ctx))
if actorId == 0 {
return gerror.New("未获取到当前操作人")
}
if request.ApplicantId == actorId {
return gerror.New("申请人不能审批自己的申请")
}
if request.Version != in.Version {
return gerror.New("审批数据已更新,请刷新后重试")
}
if request.ApprovalStatus != consts.ApprovalStatusPending && request.ApprovalStatus != consts.ApprovalStatusReviewing {
return gerror.New("当前审批状态不允许处理")
}
if request.ExpireAt != nil && request.ExpireAt.Before(gtime.Now()) {
return gerror.New("审批已超过有效期")
}
count, err := dao.ApprovalAction.Ctx(ctx).
Where(dao.ApprovalAction.Columns().ApprovalRequestId, request.Id).
Where(dao.ApprovalAction.Columns().ActorId, actorId).
WhereIn(dao.ApprovalAction.Columns().Action, []int{consts.ApprovalActionApprove, consts.ApprovalActionReject}).Count()
if err != nil {
return gerror.Wrap(err, "检查重复审批失败")
}
if count > 0 {
return gerror.New("请勿重复审批")
}
stepNo, err := s.nextStep(ctx, request.Id)
if err != nil {
return err
}
action := consts.ApprovalActionReject
status := consts.ApprovalStatusRejected
decided := true
if approved {
action = consts.ApprovalActionApprove
approvedCount, countErr := dao.ApprovalAction.Ctx(ctx).
Where(dao.ApprovalAction.Columns().ApprovalRequestId, request.Id).
Where(dao.ApprovalAction.Columns().Action, consts.ApprovalActionApprove).Count()
if countErr != nil {
return gerror.Wrap(countErr, "统计审批进度失败")
}
if uint(approvedCount+1) >= request.RequiredApprovals {
status = consts.ApprovalStatusApproved
} else {
status = consts.ApprovalStatusReviewing
decided = false
}
}
if err = s.updateStatus(ctx, request, status, gtime.Now(), decided); err != nil {
return err
}
return s.appendAction(ctx, request, stepNo, action, in.ReasonCode, in.Comment, in.MfaVerified)
})
}
func (s *sSysApproval) lockRequest(ctx context.Context, id uint64) (*entity.ApprovalRequest, error) {
var request *entity.ApprovalRequest
err := s.Model(ctx).WherePri(id).LockUpdate().Scan(&request)
if err != nil {
return nil, gerror.Wrap(err, "读取审批数据失败")
}
if request == nil || request.Id == 0 {
return nil, gerror.New("审批记录不存在或无权操作")
}
return request, nil
}
func (s *sSysApproval) updateStatus(ctx context.Context, request *entity.ApprovalRequest, status int, now *gtime.Time, decided bool) error {
data := g.Map{
dao.ApprovalRequest.Columns().ApprovalStatus: status,
dao.ApprovalRequest.Columns().UpdatedBy: contexts.GetUserId(ctx),
dao.ApprovalRequest.Columns().Version: request.Version + 1,
}
if status == consts.ApprovalStatusPending {
data[dao.ApprovalRequest.Columns().SubmittedAt] = now
}
if decided {
data[dao.ApprovalRequest.Columns().DecidedAt] = now
}
result, err := s.Model(ctx).
WherePri(request.Id).
Where(dao.ApprovalRequest.Columns().Version, request.Version).
Data(data).Update()
if err != nil {
return gerror.Wrap(err, "更新审批状态失败")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "确认审批更新结果失败")
}
if affected != 1 {
return gerror.New("审批数据已更新,请刷新后重试")
}
return nil
}
func (s *sSysApproval) nextStep(ctx context.Context, requestId uint64) (uint, error) {
count, err := dao.ApprovalAction.Ctx(ctx).
Where(dao.ApprovalAction.Columns().ApprovalRequestId, requestId).Count()
if err != nil {
return 0, gerror.Wrap(err, "获取审批步骤失败")
}
return uint(count + 1), nil
}
func (s *sSysApproval) appendAction(ctx context.Context, request *entity.ApprovalRequest, stepNo uint, action int, reasonCode, comment string, mfaVerified int) error {
snapshot := gjson.New(request)
_, err := dao.ApprovalAction.Ctx(ctx).
Hook(hook.SaveTenant).
Data(g.Map{
dao.ApprovalAction.Columns().ApprovalRequestId: request.Id,
dao.ApprovalAction.Columns().StepNo: stepNo,
dao.ApprovalAction.Columns().ActorId: contexts.GetUserId(ctx),
dao.ApprovalAction.Columns().Action: action,
dao.ApprovalAction.Columns().ReasonCode: reasonCode,
dao.ApprovalAction.Columns().Comment: comment,
dao.ApprovalAction.Columns().SnapshotJson: snapshot,
dao.ApprovalAction.Columns().MfaVerified: mfaVerified,
}).Insert()
if err != nil {
return gerror.Wrap(err, "记录审批动作失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{
Module: "approval", Action: "approval.action", TargetType: "approval_request",
TargetId: request.Id, Before: request, After: g.Map{"action": action, "stepNo": stepNo},
ChangeJson: gjson.New(g.Map{"action": action, "stepNo": stepNo, "reasonCode": reasonCode, "comment": comment}),
Reason: comment, ApprovalNo: request.ApprovalNo,
})
}
+128
View File
@@ -0,0 +1,128 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/dict"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/storager"
"hotgo/internal/model"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/format"
)
type sSysAttachment struct{}
func NewSysAttachment() *sSysAttachment {
return &sSysAttachment{}
}
func init() {
service.RegisterSysAttachment(NewSysAttachment())
dict.RegisterFunc("AttachmentKindOption", "上传类型选项", service.SysAttachment().AttachmentKindOption)
}
// Model ORM模型
func (s *sSysAttachment) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(dao.SysAttachment.Ctx(ctx), option...)
}
// Delete 删除附件
func (s *sSysAttachment) Delete(ctx context.Context, in *sysin.AttachmentDeleteInp) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Delete(); err != nil {
err = gerror.Wrap(err, "删除附件失败,请稍后重试!")
}
return
}
// View 获取附件信息
func (s *sSysAttachment) View(ctx context.Context, in *sysin.AttachmentViewInp) (res *sysin.AttachmentViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取附件信息失败,请稍后重试!")
}
return
}
// List 获取附件列表
func (s *sSysAttachment) List(ctx context.Context, in *sysin.AttachmentListInp) (list []*sysin.AttachmentListModel, totalCount int, err error) {
mod := s.Model(ctx)
memberId := contexts.GetUserId(ctx)
// 超管允许查看指定用户的附件
if service.AdminMember().VerifySuperId(ctx, memberId) && in.MemberId > 0 {
mod = mod.Where(dao.SysAttachment.Columns().MemberId, in.MemberId)
} else {
mod = mod.Where(dao.SysAttachment.Columns().MemberId, memberId)
}
if in.Drive != "" {
mod = mod.Where(dao.SysAttachment.Columns().Drive, in.Drive)
}
if in.Name != "" {
mod = mod.WhereLike(dao.SysAttachment.Columns().Name, "%"+in.Name+"%")
}
if in.Status > 0 {
mod = mod.Where(dao.SysAttachment.Columns().Status, in.Status)
}
if len(in.UpdatedAt) == 2 {
mod = mod.WhereBetween(dao.SysAttachment.Columns().UpdatedAt, in.UpdatedAt[0], in.UpdatedAt[1])
}
if in.Kind != "" {
mod = mod.Where(dao.SysAttachment.Columns().Kind, in.Kind)
}
totalCount, err = mod.Count()
if err != nil {
err = gerror.Wrap(err, "获取附件数据行失败!")
return
}
if totalCount == 0 {
return
}
if err = mod.Page(in.Page, in.PerPage).OrderDesc(dao.SysAttachment.Columns().UpdatedAt).Scan(&list); err != nil {
err = gerror.Wrap(err, "获取附件列表失败!")
return
}
for _, v := range list {
v.SizeFormat = format.FileSize(v.Size)
v.FileUrl = storager.LastUrl(ctx, v.FileUrl, v.Drive)
}
return
}
// ClearKind 清空上传类型
func (s *sSysAttachment) ClearKind(ctx context.Context, in *sysin.AttachmentClearKindInp) (err error) {
memberId := contexts.GetUserId(ctx)
if _, err = s.Model(ctx).Where(dao.SysAttachment.Columns().MemberId, memberId).Where(dao.SysAttachment.Columns().Kind, in.Kind).Delete(); err != nil {
err = gerror.Wrap(err, "删除附件上传类型失败,请稍后重试!")
}
return
}
// AttachmentKindOption 上传类型选项
func (s *sSysAttachment) AttachmentKindOption(ctx context.Context) (opts []*model.Option, err error) {
opts = append(opts, dict.GenDefaultOption("", "全部"))
opts = append(opts, dict.GenSuccessOption(storager.KindImg, "图片", "PictureOutlined"))
opts = append(opts, dict.GenPrimaryOption(storager.KindDoc, "文档", "FileWordOutlined"))
opts = append(opts, dict.GenSuccessOption(storager.KindAudio, "音频", "CustomerServiceOutlined"))
opts = append(opts, dict.GenSuccessOption(storager.KindVideo, "视频", "PlaySquareOutlined"))
opts = append(opts, dict.GenSuccessOption(storager.KindZip, "压缩包", "FileZipOutlined"))
opts = append(opts, dict.GenSuccessOption(storager.KindOther, "其他", "PlusOutlined"))
return
}
+149
View File
@@ -0,0 +1,149 @@
package sys
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/grand"
)
const bizAuditActorAdmin = 1
type sSysBizAudit struct{}
func NewSysBizAudit() *sSysBizAudit { return &sSysBizAudit{} }
func init() { service.RegisterSysBizAudit(NewSysBizAudit()) }
func (s *sSysBizAudit) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{FilterTenant: true})
}
return handler.Model(dao.BizAuditLog.Ctx(ctx), option...)
}
func (s *sSysBizAudit) Record(ctx context.Context, in *sysin.BizAuditRecordInp) error {
if in.Module == "" || in.Action == "" || in.TargetType == "" {
return gerror.New("审计模块、动作和目标类型不能为空")
}
var previous *entity.BizAuditLog
if err := s.Model(ctx).
Fields(dao.BizAuditLog.Columns().Id, dao.BizAuditLog.Columns().RowHash).
OrderDesc(dao.BizAuditLog.Columns().Id).
Limit(1).LockUpdate().Scan(&previous); err != nil {
return gerror.Wrap(err, "读取审计链失败")
}
prevHash := ""
if previous != nil {
prevHash = previous.RowHash
}
beforeDigest, err := digestValue(in.Before)
if err != nil {
return gerror.Wrap(err, "生成变更前摘要失败")
}
afterDigest, err := digestValue(in.After)
if err != nil {
return gerror.Wrap(err, "生成变更后摘要失败")
}
auditNo := "AU" + gtime.Now().Format("YmdHis") + grand.S(6)
createdAt := gtime.Now()
hashSource := struct {
AuditNo, Module, Action, TargetType, BeforeDigest, AfterDigest, ApprovalNo, TraceId, PrevHash string
ActorId, TargetId uint64
CreatedAt int64
}{
AuditNo: auditNo, Module: in.Module, Action: in.Action, TargetType: in.TargetType,
BeforeDigest: beforeDigest, AfterDigest: afterDigest, ApprovalNo: in.ApprovalNo,
TraceId: gctx.CtxId(ctx), PrevHash: prevHash, ActorId: uint64(contexts.GetUserId(ctx)),
TargetId: in.TargetId, CreatedAt: createdAt.TimestampMilli(),
}
rowHash, err := digestValue(hashSource)
if err != nil {
return gerror.Wrap(err, "生成审计行哈希失败")
}
columns := dao.BizAuditLog.Columns()
_, err = s.Model(ctx, &handler.Option{}).Hook(hook.SaveTenant).Data(gdb.Map{
columns.AuditNo: auditNo, columns.ActorType: bizAuditActorAdmin,
columns.ActorId: contexts.GetUserId(ctx), columns.Module: in.Module,
columns.Action: in.Action, columns.TargetType: in.TargetType, columns.TargetId: in.TargetId,
columns.BeforeDigest: beforeDigest, columns.AfterDigest: afterDigest, columns.ChangeJson: in.ChangeJson,
columns.Reason: in.Reason, columns.ApprovalNo: in.ApprovalNo, columns.TraceId: gctx.CtxId(ctx),
columns.PrevHash: prevHash, columns.RowHash: rowHash, columns.CreatedAt: createdAt,
}).Insert()
if err != nil {
return gerror.Wrap(err, "写入业务审计日志失败")
}
return nil
}
func (s *sSysBizAudit) List(ctx context.Context, in *sysin.BizAuditListInp) (list []*sysin.BizAuditListModel, totalCount int, err error) {
mod := s.Model(ctx).Fields(sysin.BizAuditListModel{})
columns := dao.BizAuditLog.Columns()
if in.AuditNo != "" {
mod = mod.WhereLike(columns.AuditNo, "%"+in.AuditNo+"%")
}
if in.Module != "" {
mod = mod.Where(columns.Module, in.Module)
}
if in.Action != "" {
mod = mod.Where(columns.Action, in.Action)
}
if in.TargetType != "" {
mod = mod.Where(columns.TargetType, in.TargetType)
}
if in.TargetId > 0 {
mod = mod.Where(columns.TargetId, in.TargetId)
}
if in.ApprovalNo != "" {
mod = mod.WhereLike(columns.ApprovalNo, "%"+in.ApprovalNo+"%")
}
if in.Actor != "" {
ids, queryErr := service.AdminMember().GetIdsByKeyword(ctx, in.Actor)
if queryErr != nil {
return nil, 0, queryErr
}
mod = mod.WhereIn(columns.ActorId, ids)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(columns.CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
if err = mod.Page(in.Page, in.PerPage).OrderDesc(columns.Id).ScanAndCount(&list, &totalCount, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取业务审计日志失败")
}
return
}
func (s *sSysBizAudit) View(ctx context.Context, in *sysin.BizAuditViewInp) (res *sysin.BizAuditViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取业务审计详情失败")
}
if res == nil || res.Id == 0 {
return nil, gerror.New("业务审计日志不存在或无权查看")
}
return
}
func digestValue(value any) (string, error) {
if value == nil {
return "", nil
}
data, err := json.Marshal(value)
if err != nil {
return "", err
}
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:]), nil
}
+167
View File
@@ -0,0 +1,167 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"github.com/gogf/gf/v2/database/gredis"
"github.com/gogf/gf/v2/errors/gcode"
"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"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/global"
"hotgo/internal/library/location"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/convert"
"sync"
)
type sSysBlacklist struct {
sync.RWMutex
list map[string]struct{}
}
func NewSysBlacklist() *sSysBlacklist {
return &sSysBlacklist{
list: make(map[string]struct{}),
}
}
func init() {
service.RegisterSysBlacklist(NewSysBlacklist())
}
// Delete 删除
func (s *sSysBlacklist) Delete(ctx context.Context, in *sysin.BlacklistDeleteInp) (err error) {
defer s.VariableLoad(ctx, err)
_, err = dao.SysBlacklist.Ctx(ctx).Where("id", in.Id).Delete()
return
}
// Edit 修改/新增
func (s *sSysBlacklist) Edit(ctx context.Context, in *sysin.BlacklistEditInp) (err error) {
defer s.VariableLoad(ctx, err)
if in.Ip == "" {
err = gerror.New("ip不能为空")
return
}
// 修改
if in.Id > 0 {
_, err = dao.SysBlacklist.Ctx(ctx).Where("id", in.Id).Data(in).Update()
return
}
// 新增
_, err = dao.SysBlacklist.Ctx(ctx).Data(in).OmitEmptyData().Insert()
return
}
// Status 更新状态
func (s *sSysBlacklist) Status(ctx context.Context, in *sysin.BlacklistStatusInp) (err error) {
defer s.VariableLoad(ctx, err)
// 修改
_, err = dao.SysBlacklist.Ctx(ctx).Where("id", in.Id).Data("status", in.Status).Update()
return
}
// View 获取指定信息
func (s *sSysBlacklist) View(ctx context.Context, in *sysin.BlacklistViewInp) (res *sysin.BlacklistViewModel, err error) {
err = dao.SysBlacklist.Ctx(ctx).Where("id", in.Id).Scan(&res)
return
}
// List 获取列表
func (s *sSysBlacklist) List(ctx context.Context, in *sysin.BlacklistListInp) (list []*sysin.BlacklistListModel, totalCount int, err error) {
mod := dao.SysBlacklist.Ctx(ctx)
cols := dao.SysBlacklist.Columns()
if in.Ip != "" {
mod = mod.WhereLike(cols.Ip, "%"+in.Ip+"%")
}
if in.Remark != "" {
mod = mod.WhereLike(cols.Remark, "%"+in.Remark+"%")
}
if in.Status > 0 {
mod = mod.Where(cols.Status, in.Status)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(cols.CreatedAt, gtime.New(in.CreatedAt[0]), gtime.New(in.CreatedAt[1]))
}
totalCount, err = mod.Count()
if err != nil {
return
}
if totalCount == 0 {
return
}
if err = mod.Page(in.Page, in.PerPage).Order("id desc").Scan(&list); err != nil {
return
}
return
}
// VariableLoad 变化加载
func (s *sSysBlacklist) VariableLoad(ctx context.Context, err error) {
if err == nil {
s.Load(ctx)
global.PublishClusterSync(ctx, consts.ClusterSyncSysBlacklist, nil)
}
}
// Load 加载黑名单
func (s *sSysBlacklist) Load(ctx context.Context) {
s.RLock()
defer s.RUnlock()
s.list = make(map[string]struct{})
array, err := dao.SysBlacklist.Ctx(ctx).
Fields(dao.SysBlacklist.Columns().Ip).
Where(dao.SysBlacklist.Columns().Status, consts.StatusEnabled).
Array()
if err != nil {
g.Log().Errorf(ctx, "load blacklist fail:%+v", err)
return
}
for _, v := range array {
list := convert.IpFilterStrategy(v.String())
if len(list) > 0 {
for k := range list {
s.list[k] = struct{}{}
}
}
}
}
// VerifyRequest 验证请求的访问IP是否在黑名单,如果存在则返回错误
func (s *sSysBlacklist) VerifyRequest(r *ghttp.Request) (err error) {
if len(s.list) == 0 {
return
}
if _, ok := s.list[location.GetClientIp(r)]; ok {
err = gerror.NewCode(gcode.New(gcode.CodeServerBusy.Code(), "请求异常,已被封禁,如有疑问请联系管理员!", nil))
return
}
return
}
// ClusterSync 集群同步
func (s *sSysBlacklist) ClusterSync(ctx context.Context, message *gredis.Message) {
s.Load(ctx)
}
+332
View File
@@ -0,0 +1,332 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/global"
"hotgo/internal/library/payment"
"hotgo/internal/library/sms"
"hotgo/internal/library/storager"
"hotgo/internal/library/token"
"hotgo/internal/library/wechat"
"hotgo/internal/model"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/simple"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/database/gredis"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysConfig struct{}
func NewSysConfig() *sSysConfig {
return &sSysConfig{}
}
func init() {
service.RegisterSysConfig(NewSysConfig())
}
// InitConfig 初始化系统配置
func (s *sSysConfig) InitConfig(ctx context.Context) {
if err := s.LoadConfig(ctx); err != nil {
g.Log().Fatalf(ctx, "InitConfig fail:%+v", err)
}
}
// LoadConfig 加载系统配置
func (s *sSysConfig) LoadConfig(ctx context.Context) (err error) {
wx, err := s.GetWechat(ctx)
if err != nil {
return
}
wechat.SetConfig(wx)
pay, err := s.GetPay(ctx)
if err != nil {
return
}
payment.SetConfig(pay)
upload, err := s.GetUpload(ctx)
if err != nil {
return
}
storager.SetConfig(upload)
sm, err := s.GetSms(ctx)
if err != nil {
return
}
sms.SetConfig(sm)
tk, err := s.GetLoadToken(ctx)
if err != nil {
return
}
token.SetConfig(tk)
// 更多
// ...
return
}
// GetLogin 获取登录配置
func (s *sSysConfig) GetLogin(ctx context.Context) (conf *model.LoginConfig, err error) {
models, err := s.GetConfigByGroup(ctx, &sysin.GetConfigInp{Group: "login"})
if err != nil {
return
}
err = gconv.Scan(models.List, &conf)
return
}
// GetWechat 获取微信配置
func (s *sSysConfig) GetWechat(ctx context.Context) (conf *model.WechatConfig, err error) {
models, err := s.GetConfigByGroup(ctx, &sysin.GetConfigInp{Group: "wechat"})
if err != nil {
return
}
err = gconv.Scan(models.List, &conf)
return
}
// GetPay 获取支付配置
func (s *sSysConfig) GetPay(ctx context.Context) (conf *model.PayConfig, err error) {
models, err := s.GetConfigByGroup(ctx, &sysin.GetConfigInp{Group: "pay"})
if err != nil {
return
}
err = gconv.Scan(models.List, &conf)
return
}
// GetSms 获取短信配置
func (s *sSysConfig) GetSms(ctx context.Context) (conf *model.SmsConfig, err error) {
models, err := s.GetConfigByGroup(ctx, &sysin.GetConfigInp{Group: "sms"})
if err != nil {
return
}
err = gconv.Scan(models.List, &conf)
return
}
// GetGeo 获取地理配置
func (s *sSysConfig) GetGeo(ctx context.Context) (conf *model.GeoConfig, err error) {
models, err := s.GetConfigByGroup(ctx, &sysin.GetConfigInp{Group: "geo"})
if err != nil {
return
}
err = gconv.Scan(models.List, &conf)
return
}
// GetUpload 获取上传配置
func (s *sSysConfig) GetUpload(ctx context.Context) (conf *model.UploadConfig, err error) {
models, err := s.GetConfigByGroup(ctx, &sysin.GetConfigInp{Group: "upload"})
if err != nil {
return
}
err = gconv.Scan(models.List, &conf)
return
}
// GetSmtp 获取邮件配置
func (s *sSysConfig) GetSmtp(ctx context.Context) (conf *model.EmailConfig, err error) {
models, err := s.GetConfigByGroup(ctx, &sysin.GetConfigInp{Group: "smtp"})
if err != nil {
return
}
if err = gconv.Scan(models.List, &conf); err != nil {
return
}
conf.Addr = fmt.Sprintf("%s:%d", conf.Host, conf.Port)
return
}
// 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
}
err = gconv.Scan(models.List, &conf)
return
}
// GetLoadTCP 获取本地tcp配置
func (s *sSysConfig) GetLoadTCP(ctx context.Context) (conf *model.TCPConfig, err error) {
err = g.Cfg().MustGet(ctx, "tcp").Scan(&conf)
return
}
// GetLoadGenerate 获取本地生成配置
func (s *sSysConfig) GetLoadGenerate(ctx context.Context) (conf *model.GenerateConfig, err error) {
err = g.Cfg().MustGet(ctx, "hggen").Scan(&conf)
return
}
// GetLoadToken 获取本地token配置
func (s *sSysConfig) GetLoadToken(ctx context.Context) (conf *model.TokenConfig, err error) {
err = g.Cfg().MustGet(ctx, "token").Scan(&conf)
return
}
// GetLoadLog 获取本地日志配置
func (s *sSysConfig) GetLoadLog(ctx context.Context) (conf *model.LogConfig, err error) {
err = g.Cfg().MustGet(ctx, "system.log").Scan(&conf)
return
}
// GetLoadServeLog 获取本地服务日志配置
func (s *sSysConfig) GetLoadServeLog(ctx context.Context) (conf *model.ServeLogConfig, err error) {
err = g.Cfg().MustGet(ctx, "system.serveLog").Scan(&conf)
return
}
// GetConfigByGroup 获取指定分组的配置
func (s *sSysConfig) GetConfigByGroup(ctx context.Context, in *sysin.GetConfigInp) (res *sysin.GetConfigModel, err error) {
if in.Group == "" {
err = gerror.New("分组不能为空")
return
}
var models []*entity.SysConfig
cols := dao.SysConfig.Columns()
if err = dao.SysConfig.Ctx(ctx).Fields(cols.Key, cols.Value, cols.Type).Where(cols.Group, in.Group).Scan(&models); err != nil {
err = gerror.Wrapf(err, "获取配置分组[ %v ]失败,请稍后重试!", in.Group)
return
}
res = new(sysin.GetConfigModel)
if len(models) > 0 {
res.List = make(g.Map, len(models))
for _, v := range models {
val, err := s.ConversionType(ctx, v)
if err != nil {
return nil, err
}
res.List[v.Key] = val
}
}
res.List = simple.FilterMaskDemo(ctx, res.List)
return
}
// ConversionType 转换类型
func (s *sSysConfig) ConversionType(ctx context.Context, models *entity.SysConfig) (value interface{}, err error) {
if models == nil {
err = gerror.New("数据不存在")
return
}
return consts.ConvType(models.Value, models.Type), nil
}
// UpdateConfigByGroup 更新指定分组的配置
func (s *sSysConfig) UpdateConfigByGroup(ctx context.Context, in *sysin.UpdateConfigInp) (err error) {
if in.Group == "" {
err = gerror.New("分组不能为空")
return
}
var (
mod = dao.SysConfig.Ctx(ctx)
models []*entity.SysConfig
)
if err = mod.Where("group", in.Group).Scan(&models); err != nil {
return
}
err = dao.SysConfig.Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
for k, v := range in.List {
row := s.getConfigByKey(k, models)
// 新增
if row == nil {
err = gerror.Newf("暂不支持从前台添加变量,请先在数据库表[%v]中配置变量:%v", dao.SysConfig.Table(), k)
return
}
// 更新
_, err = dao.SysConfig.Ctx(ctx).Where("id", row.Id).Data(g.Map{"value": v, "updated_at": gtime.Now()}).Update()
if err != nil {
return
}
}
return s.syncUpdate(ctx, in)
})
if err != nil {
return
}
global.PublishClusterSync(ctx, consts.ClusterSyncSysconfig, nil)
return
}
func (s *sSysConfig) getConfigByKey(key string, models []*entity.SysConfig) *entity.SysConfig {
if len(models) == 0 {
return nil
}
for _, v := range models {
if key == v.Key {
return v
}
}
return nil
}
// syncUpdate 同步更新一些加载配置
func (s *sSysConfig) syncUpdate(ctx context.Context, in *sysin.UpdateConfigInp) (err error) {
var cfg any
switch in.Group {
case "wechat":
cfg, err = s.GetWechat(ctx)
if err == nil {
wechat.SetConfig(cfg.(*model.WechatConfig))
}
case "pay":
cfg, err = s.GetPay(ctx)
if err == nil {
payment.SetConfig(cfg.(*model.PayConfig))
}
case "upload":
cfg, err = s.GetUpload(ctx)
if err == nil {
storager.SetConfig(cfg.(*model.UploadConfig))
}
case "sms":
cfg, err = s.GetSms(ctx)
if err == nil {
sms.SetConfig(cfg.(*model.SmsConfig))
}
}
if err != nil {
err = gerror.Newf("syncUpdate %v conifg fail:%+v", in.Group, err.Error())
}
return
}
// ClusterSync 集群同步
func (s *sSysConfig) ClusterSync(ctx context.Context, message *gredis.Message) {
if err := s.LoadConfig(ctx); err != nil {
g.Log().Errorf(ctx, "ClusterSync fail:%+v", err)
}
}
+244
View File
@@ -0,0 +1,244 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/cron"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/simple"
"hotgo/utility/validate"
"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"
)
type sSysCron struct{}
func NewSysCron() *sSysCron {
return &sSysCron{}
}
func init() {
service.RegisterSysCron(NewSysCron())
}
func (s *sSysCron) StartCron(ctx context.Context) {
var list []*entity.SysCron
if err := dao.SysCron.Ctx(ctx).
Where("status", consts.StatusEnabled).
Order("sort asc,id desc").
Scan(&list); err != nil {
cron.Logger().Fatalf(ctx, "定时任务获取失败, err . %v", err)
return
}
if err := cron.StartALL(list); err != nil {
cron.Logger().Fatalf(ctx, "定时任务启动失败, err . %v", err)
return
}
}
// Delete 删除
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
}
if models == nil {
err = gerror.New("定时任务不存在或已被删除")
return
}
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
if _, err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Delete(); err != nil {
return
}
return cron.Delete(models)
})
return
}
// Edit 修改/新增
func (s *sSysCron) Edit(ctx context.Context, in *sysin.CronEditInp) (err error) {
if in.Name == "" {
err = gerror.New("标题不能为空")
return
}
// 修改
if in.Id > 0 {
if _, err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Data(in).Update(); err != nil {
return
}
simple.SafeGo(ctx, func(ctx context.Context) {
_ = cron.RefreshStatus(&in.SysCron)
})
return
}
// 新增
in.SysCron.Id, err = dao.SysCron.Ctx(ctx).Data(in).OmitEmptyData().InsertAndGetId()
if err != nil || in.SysCron.Id < 1 {
return
}
if in.SysCron.Status == consts.StatusEnabled {
simple.SafeGo(ctx, func(ctx context.Context) {
_ = cron.Start(&in.SysCron)
})
}
return
}
// Status 更新状态
func (s *sSysCron) Status(ctx context.Context, in *sysin.CronStatusInp) (err error) {
if in.Id <= 0 {
err = gerror.New("ID不能为空")
return
}
if in.Status <= 0 {
err = gerror.New("状态不能为空")
return
}
if !validate.InSlice(consts.StatusSlice, in.Status) {
err = gerror.New("状态不正确")
return
}
var models *entity.SysCron
if err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Scan(&models); err != nil {
return
}
if models == nil {
err = gerror.New("定时任务不存在")
return
}
_, err = dao.SysCron.Ctx(ctx).Where("id", in.Id).Data("status", in.Status).Update()
if err != nil {
return
}
models.Status = in.Status
simple.SafeGo(ctx, func(ctx context.Context) {
_ = cron.RefreshStatus(models)
})
return
}
// MaxSort 最大排序
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 {
return
}
if res == nil {
res = new(sysin.CronMaxSortModel)
}
res.Sort = form.DefaultMaxSort(res.Sort)
return
}
// 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 {
return
}
return
}
// List 获取列表
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.GroupId > 0 {
mod = mod.Where("group_id", in.GroupId)
}
if in.Status > 0 {
mod = mod.Where("status", in.Status)
}
totalCount, err = mod.Count()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
if totalCount == 0 {
return
}
if err = mod.Page(in.Page, in.PerPage).Order("id desc").Scan(&list); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
for _, v := range list {
v.GroupName, _ = s.GetName(ctx, v.GroupId)
}
return
}
// GetName 获取分组名称
func (s *sSysCron) GetName(ctx context.Context, id int64) (name string, err error) {
m := dao.SysCronGroup.Ctx(ctx).Fields(dao.SysCronGroup.Columns().Name).Where(dao.SysCronGroup.Columns().Id, id)
list, err := m.Value()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return name, err
}
return list.String(), nil
}
// OnlineExec 在线执行
func (s *sSysCron) OnlineExec(ctx context.Context, in *sysin.OnlineExecInp) (err error) {
var data *entity.SysCron
if err = dao.SysCron.Ctx(ctx).Where(dao.SysCron.Columns().Id, in.Id).Scan(&data); err != nil {
return
}
if data == nil {
err = gerror.New("定时任务不存在")
return
}
return cron.Once(gctx.New(), data)
}
// DispatchLog 查看指定任务的调度日志
func (s *sSysCron) DispatchLog(ctx context.Context, in *sysin.DispatchLogInp) (res *sysin.DispatchLogModel, err error) {
var data *entity.SysCron
if err = dao.SysCron.Ctx(ctx).Where(dao.SysCron.Columns().Id, in.Id).Scan(&data); err != nil {
return
}
if data == nil {
err = gerror.New("定时任务不存在")
return
}
res = new(sysin.DispatchLogModel)
res.Log, err = cron.DispatchLog(data)
return
}
+161
View File
@@ -0,0 +1,161 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
)
type sSysCronGroup struct{}
func NewSysCronGroup() *sSysCronGroup {
return &sSysCronGroup{}
}
func init() {
service.RegisterSysCronGroup(NewSysCronGroup())
}
// Delete 删除
func (s *sSysCronGroup) Delete(ctx context.Context, in *sysin.CronGroupDeleteInp) (err error) {
_, err = dao.SysCronGroup.Ctx(ctx).Where("id", in.Id).Delete()
return
}
// Edit 修改/新增
func (s *sSysCronGroup) Edit(ctx context.Context, in *sysin.CronGroupEditInp) (err error) {
// 修改
if in.Id > 0 {
if _, err = dao.SysCronGroup.Ctx(ctx).Fields(sysin.CronGroupUpdateFields{}).Where("id", in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
}
return
}
// 新增
if _, err = dao.SysCronGroup.Ctx(ctx).Fields(sysin.CronGroupInsertFields{}).Data(in).OmitEmptyData().Insert(); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
}
return
}
// Status 更新状态
func (s *sSysCronGroup) Status(ctx context.Context, in *sysin.CronGroupStatusInp) (err error) {
if _, err = dao.SysCronGroup.Ctx(ctx).Where("id", in.Id).Data("status", in.Status).Update(); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
}
return
}
// MaxSort 最大排序
func (s *sSysCronGroup) MaxSort(ctx context.Context, in *sysin.CronGroupMaxSortInp) (res *sysin.CronGroupMaxSortModel, err error) {
if in.Id > 0 {
if err = dao.SysCronGroup.Ctx(ctx).Where("id", in.Id).Order("sort desc").Scan(&res); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
}
if res == nil {
res = new(sysin.CronGroupMaxSortModel)
}
res.Sort = form.DefaultMaxSort(res.Sort)
return
}
// View 获取指定信息
func (s *sSysCronGroup) View(ctx context.Context, in *sysin.CronGroupViewInp) (res *sysin.CronGroupViewModel, err error) {
if err = dao.SysCronGroup.Ctx(ctx).Where("id", in.Id).Scan(&res); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
}
return
}
// List 获取列表
func (s *sSysCronGroup) List(ctx context.Context, in *sysin.CronGroupListInp) (list []*sysin.CronGroupListModel, totalCount int, err error) {
mod := dao.SysCronGroup.Ctx(ctx)
if in.Name != "" {
mod = mod.WhereLike("name", "%"+in.Name+"%")
}
if in.Status > 0 {
mod = mod.Where("status", in.Status)
}
totalCount, err = mod.Count()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
if totalCount == 0 {
return
}
if err = mod.Page(in.Page, in.PerPage).OrderDesc(dao.SysCronGroup.Columns().Id).Scan(&list); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
}
for _, v := range list {
if v.Pid < 1 {
continue
}
name, err := dao.SysCronGroup.Ctx(ctx).Fields(dao.SysCronGroup.Columns().Name).WherePri(v.Pid).Value()
if err != nil {
return nil, 0, err
}
v.SupName = name.String()
}
return
}
// Select 选项
func (s *sSysCronGroup) Select(ctx context.Context, in *sysin.CronGroupSelectInp) (res *sysin.CronGroupSelectModel, err error) {
var (
mod = dao.SysCronGroup.Ctx(ctx)
models []*entity.SysCronGroup
)
if err = mod.Order("pid asc,sort asc").Scan(&models); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
res = new(sysin.CronGroupSelectModel)
res.List = s.treeList(0, models)
return
}
// treeList 树状列表
func (s *sSysCronGroup) treeList(pid int64, nodes []*entity.SysCronGroup) (list []*sysin.CronGroupTree) {
list = make([]*sysin.CronGroupTree, 0)
for _, v := range nodes {
if v.Pid == pid {
item := new(sysin.CronGroupTree)
item.SysCronGroup = *v
item.Label = v.Name
item.Value = v.Id
item.Key = v.Id
child := s.treeList(v.Id, nodes)
if len(child) > 0 {
item.Children = child
}
list = append(list, item)
}
}
return
}
+240
View File
@@ -0,0 +1,240 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2025 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
// @AutoGenerate Version 2.18.6
package sys
import (
"context"
"fmt"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/convert"
"hotgo/utility/excel"
"hotgo/utility/validate"
"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"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysCurdDemo struct{}
func NewSysCurdDemo() *sSysCurdDemo {
return &sSysCurdDemo{}
}
func init() {
service.RegisterSysCurdDemo(NewSysCurdDemo())
}
// Model CURD列表ORM模型
func (s *sSysCurdDemo) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(dao.SysGenCurdDemo.Ctx(ctx), option...)
}
// List 获取CURD列表列表
func (s *sSysCurdDemo) List(ctx context.Context, in *sysin.CurdDemoListInp) (list []*sysin.CurdDemoListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.FieldsPrefix(dao.SysGenCurdDemo.Table(), sysin.CurdDemoListModel{})
mod = mod.Fields(hgorm.JoinFields(ctx, sysin.CurdDemoListModel{}, &dao.TestCategory, "testCategory"))
// 关联表字段
mod = mod.LeftJoinOnFields(dao.TestCategory.Table(), dao.SysGenCurdDemo.Columns().CategoryId, "=", dao.TestCategory.Columns().Id)
// 查询ID
if in.Id > 0 {
mod = mod.Where(dao.SysGenCurdDemo.Columns().Id, in.Id)
}
// 查询标题
if in.Title != "" {
mod = mod.WhereLike(dao.SysGenCurdDemo.Columns().Title, "%"+in.Title+"%")
}
// 查询描述
if in.Description != "" {
mod = mod.WhereLike(dao.SysGenCurdDemo.Columns().Description, "%"+in.Description+"%")
}
// 查询创建者
if in.CreatedBy != "" {
ids, err := service.AdminMember().GetIdsByKeyword(ctx, in.CreatedBy)
if err != nil {
return nil, 0, err
}
mod = mod.WhereIn(dao.SysGenCurdDemo.Columns().CreatedBy, ids)
}
// 查询删除者
if in.DeletedBy != "" {
ids, err := service.AdminMember().GetIdsByKeyword(ctx, in.DeletedBy)
if err != nil {
return nil, 0, err
}
mod = mod.WhereIn(dao.SysGenCurdDemo.Columns().DeletedBy, ids)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.SysGenCurdDemo.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 查询关联分类
if in.TestCategoryName != "" {
mod = mod.WherePrefix(dao.TestCategory.Table(), dao.TestCategory.Columns().Name, in.TestCategoryName)
}
// 分页
mod = mod.Page(in.Page, in.PerPage)
// 排序
mod = mod.OrderAsc(dao.SysGenCurdDemo.Table() + "." + dao.SysGenCurdDemo.Columns().Sort).OrderDesc(dao.SysGenCurdDemo.Table() + "." + dao.SysGenCurdDemo.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取CURD列表列表失败,请稍后重试!")
return
}
return
}
// Export 导出CURD列表
func (s *sSysCurdDemo) Export(ctx context.Context, in *sysin.CurdDemoListInp) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(sysin.CurdDemoExportModel{})
if err != nil {
return
}
var (
fileName = "导出CURD列表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, form.CalPageCount(totalCount, in.PerPage), in.Page, len(list))
exports []sysin.CurdDemoExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增CURD列表
func (s *sSysCurdDemo) Edit(ctx context.Context, in *sysin.CurdDemoEditInp) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
in.UpdatedBy = contexts.GetUserId(ctx)
if _, err = s.Model(ctx).
Fields(sysin.CurdDemoUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改CURD列表失败,请稍后重试!")
}
return
}
// 新增
in.CreatedBy = contexts.GetUserId(ctx)
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(sysin.CurdDemoInsertFields{}).
Data(in).OmitEmptyData().Insert(); err != nil {
err = gerror.Wrap(err, "新增CURD列表失败,请稍后重试!")
}
return
})
}
// Delete 删除CURD列表
func (s *sSysCurdDemo) Delete(ctx context.Context, in *sysin.CurdDemoDeleteInp) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.SysGenCurdDemo.Columns().DeletedBy: contexts.GetUserId(ctx),
dao.SysGenCurdDemo.Columns().DeletedAt: gtime.Now(),
}).Unscoped().Update(); err != nil {
err = gerror.Wrap(err, "删除CURD列表失败,请稍后重试!")
return
}
return
}
// MaxSort 获取CURD列表最大排序
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, "获取CURD列表最大排序,请稍后重试!")
return
}
if res == nil {
res = new(sysin.CurdDemoMaxSortModel)
}
res.Sort = form.DefaultMaxSort(res.Sort)
return
}
// View 获取CURD列表指定信息
func (s *sSysCurdDemo) View(ctx context.Context, in *sysin.CurdDemoViewInp) (res *sysin.CurdDemoViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取CURD列表信息,请稍后重试!")
return
}
return
}
// Status 更新CURD列表状态
func (s *sSysCurdDemo) Status(ctx context.Context, in *sysin.CurdDemoStatusInp) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.SysGenCurdDemo.Columns().Status: in.Status,
dao.SysGenCurdDemo.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新CURD列表状态失败,请稍后重试!")
return
}
return
}
// Switch 更新CURD列表开关
func (s *sSysCurdDemo) Switch(ctx context.Context, in *sysin.CurdDemoSwitchInp) (err error) {
var fields = []string{
dao.SysGenCurdDemo.Columns().Switch,
// ...
}
if !validate.InSlice(fields, in.Key) {
err = gerror.New("开关键名不在白名单")
return
}
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
in.Key: in.Value,
dao.SysGenCurdDemo.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新CURD列表开关失败,请稍后重试!")
return
}
return
}
@@ -0,0 +1,121 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"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/gtime"
)
type sSysCustomerService struct{}
func NewSysCustomerService() *sSysCustomerService { return &sSysCustomerService{} }
func init() { service.RegisterSysCustomerService(NewSysCustomerService()) }
func (s *sSysCustomerService) model(ctx context.Context) *gdb.Model {
return handler.Model(dao.CustomerService.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysCustomerService) List(ctx context.Context, in *sysin.CustomerServiceListInp) (list []*entity.CustomerService, total int, err error) {
c := dao.CustomerService.Columns()
mod := s.model(ctx)
if in.Name != "" {
mod = mod.WhereLike(c.Name, "%"+in.Name+"%")
}
if in.Status > 0 {
mod = mod.Where(c.Status, in.Status)
}
err = mod.Page(in.Page, in.PerPage).OrderAsc(c.Sort).OrderDesc(c.Id).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取客服列表失败")
}
return
}
func (s *sSysCustomerService) View(ctx context.Context, in *sysin.CustomerServiceViewInp) (res *entity.CustomerService, err error) {
if err = s.model(ctx).WherePri(in.Id).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取客服详情失败")
}
if res == nil {
return nil, gerror.New("客服配置不存在")
}
return
}
func (s *sSysCustomerService) Edit(ctx context.Context, in *sysin.CustomerServiceEditInp) error {
c := dao.CustomerService.Columns()
var before *entity.CustomerService
if in.Id > 0 {
if err := s.model(ctx).WherePri(in.Id).Scan(&before); err != nil {
return err
}
if before == nil {
return gerror.New("客服配置不存在")
}
}
count, err := s.model(ctx).Where(c.ServiceCode, in.ServiceCode).WhereNot(c.Id, in.Id).Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("客服编码已存在")
}
data := g.Map{c.ServiceCode: in.ServiceCode, c.Name: in.Name, c.AvatarUrl: in.AvatarUrl, c.WorkTime: in.WorkTime, c.OnlineStatus: in.OnlineStatus, c.ServiceUrl: in.ServiceUrl, c.BackupContact: in.BackupContact, c.SessionMapping: in.SessionMapping, c.Status: in.Status, c.Sort: in.Sort, c.Remark: in.Remark, c.UpdatedBy: contexts.GetUserId(ctx)}
if in.Id > 0 {
if _, err = s.model(ctx).WherePri(in.Id).Data(data).Update(); err != nil {
return gerror.Wrap(err, "更新客服配置失败")
}
} else {
data[c.CreatedBy] = contexts.GetUserId(ctx)
result, e := s.model(ctx).Fields(c.ServiceCode, c.Name, c.AvatarUrl, c.WorkTime, c.OnlineStatus, c.ServiceUrl, c.BackupContact, c.SessionMapping, c.Status, c.Sort, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
if e != nil {
return gerror.Wrap(e, "创建客服配置失败")
}
id, _ := result.LastInsertId()
in.Id = uint64(id)
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "customer_service", Action: "customer_service.edit", TargetType: "customer_service", TargetId: in.Id, Before: before, After: g.Map{"name": in.Name, "workTime": in.WorkTime, "onlineStatus": in.OnlineStatus, "serviceUrl": in.ServiceUrl, "status": in.Status}, Reason: in.Remark})
}
func (s *sSysCustomerService) Status(ctx context.Context, in *sysin.CustomerServiceStatusInp) error {
c := dao.CustomerService.Columns()
var row *entity.CustomerService
if err := s.model(ctx).WherePri(in.Id).Scan(&row); err != nil {
return err
}
if row == nil {
return gerror.New("客服配置不存在")
}
if _, err := s.model(ctx).WherePri(in.Id).Data(g.Map{c.Status: in.Status, c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return gerror.Wrap(err, "更新客服状态失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "customer_service", Action: "customer_service.status", TargetType: "customer_service", TargetId: in.Id, Before: g.Map{"status": row.Status}, After: g.Map{"status": in.Status}})
}
func (s *sSysCustomerService) Delete(ctx context.Context, in *sysin.CustomerServiceDeleteInp) error {
c := dao.CustomerService.Columns()
var row *entity.CustomerService
if err := s.model(ctx).WherePri(in.Id).Scan(&row); err != nil {
return err
}
if row == nil {
return gerror.New("客服配置不存在")
}
if row.Status == consts.StatusEnabled {
return gerror.New("启用中的客服不能删除,请先停用")
}
if _, err := s.model(ctx).WherePri(in.Id).Data(g.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx), c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return gerror.Wrap(err, "删除客服配置失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "customer_service", Action: "customer_service.delete", TargetType: "customer_service", TargetId: in.Id, Before: row, Reason: in.Reason})
}
+285
View File
@@ -0,0 +1,285 @@
package sys
import (
"context"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"math"
"sort"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
const (
dashboardDimensionGlobal = "global"
dashboardDimensionChannel = "channel"
)
type dashboardMetricDefinition struct {
Code string
Title string
ValueType string
Aggregation string
Description string
RouteName string
Sensitive bool
}
var dashboardMetricDefinitions = []dashboardMetricDefinition{
{Code: "member_total", Title: "用户总量", ValueType: "count", Aggregation: "latest", Description: "截至区间结束日的去重有效会员数", RouteName: ""},
{Code: "member_new", Title: "新增用户", ValueType: "count", Aggregation: "sum", Description: "区间内新增有效会员数", RouteName: ""},
{Code: "recharge_member", Title: "充值用户", ValueType: "count", Aggregation: "sum", Description: "区间内至少成功充值一次的去重会员数", RouteName: ""},
{Code: "product_total", Title: "商品总量", ValueType: "count", Aggregation: "latest", Description: "截至区间结束日的有效商品数", RouteName: ""},
{Code: "trade_order", Title: "订单量", ValueType: "count", Aggregation: "sum", Description: "区间内创建的业务订单数", RouteName: ""},
{Code: "trade_order", Title: "订单金额", ValueType: "amount", Aggregation: "sum", Description: "区间内业务订单总金额", RouteName: "", Sensitive: true},
{Code: "recharge_success", Title: "成功充值", ValueType: "amount", Aggregation: "sum", Description: "区间内已成功入账的充值金额", RouteName: "", Sensitive: true},
{Code: "recharge_pending", Title: "待处理充值", ValueType: "amount", Aggregation: "latest", Description: "截至区间结束日的待处理充值金额", RouteName: "", Sensitive: true},
{Code: "withdrawal_success", Title: "成功提现", ValueType: "amount", Aggregation: "sum", Description: "区间内已成功支付的提现金额", RouteName: "", Sensitive: true},
{Code: "withdrawal_pending", Title: "待处理提现", ValueType: "amount", Aggregation: "latest", Description: "截至区间结束日的待审核及处理中提现金额", RouteName: "", Sensitive: true},
{Code: "commission", Title: "任务佣金", ValueType: "amount", Aggregation: "sum", Description: "区间内结算的任务佣金", RouteName: "", Sensitive: true},
{Code: "referral_commission", Title: "下级返佣", ValueType: "amount", Aggregation: "sum", Description: "区间内结算的层级返佣", RouteName: "", Sensitive: true},
{Code: "interest_transfer", Title: "利息宝转入", ValueType: "amount", Aggregation: "sum", Description: "区间内转入利息宝的金额", RouteName: "", Sensitive: true},
{Code: "wallet_available", Title: "可用余额", ValueType: "amount", Aggregation: "latest", Description: "截至区间结束日的钱包可用余额", RouteName: "", Sensitive: true},
{Code: "wallet_frozen", Title: "冻结余额", ValueType: "amount", Aggregation: "latest", Description: "截至区间结束日的钱包冻结余额", RouteName: "", Sensitive: true},
}
type sSysDashboard struct{}
func NewSysDashboard() *sSysDashboard { return &sSysDashboard{} }
func init() { service.RegisterSysDashboard(NewSysDashboard()) }
func (s *sSysDashboard) Summary(ctx context.Context, in *sysin.DashboardSummaryInp) (res *sysin.DashboardSummaryModel, err error) {
start, end := normalizeDashboardRange(in)
days := int(end.Sub(start).Hours()/24) + 1
previousEnd := start.AddDate(0, 0, -1)
previousStart := previousEnd.AddDate(0, 0, -(days - 1))
currentRows, err := s.queryRows(ctx, start, end)
if err != nil {
return nil, err
}
previousRows, err := s.queryRows(ctx, previousStart, previousEnd)
if err != nil {
return nil, err
}
canViewAmounts := service.AdminRole().Verify(ctx, "/dashboard/amount", "GET")
res = &sysin.DashboardSummaryModel{
Range: sysin.DashboardRangeModel{StartDate: start.Format("Y-m-d"), EndDate: end.Format("Y-m-d")},
GeneratedAt: gtime.Now(),
Metrics: make([]*sysin.DashboardMetricModel, 0, len(dashboardMetricDefinitions)),
Trend: s.buildTrend(start, end, currentRows),
Channels: s.buildChannels(currentRows, previousRows, canViewAmounts),
}
if !canViewAmounts {
for _, point := range res.Trend {
point.OrderAmount = 0
point.RechargeAmount = 0
point.WithdrawalAmount = 0
point.CommissionAmount = 0
}
}
for _, definition := range dashboardMetricDefinitions {
value := aggregateDashboardMetric(currentRows, definition)
previousValue := aggregateDashboardMetric(previousRows, definition)
masked := definition.Sensitive && !canViewAmounts
if masked {
value = 0
previousValue = 0
}
res.Metrics = append(res.Metrics, &sysin.DashboardMetricModel{
Code: definition.Code, Title: definition.Title, Value: value, PreviousValue: previousValue,
ChangeRate: dashboardChangeRate(value, previousValue), ValueType: definition.ValueType,
Masked: masked, Description: definition.Description, RouteName: definition.RouteName,
})
}
res.DataReady, res.DataAsOf = dashboardDataStatus(currentRows)
if res.DataAsOf == nil {
res.DataAsOf = res.GeneratedAt
res.Warning = "当前业务汇总表暂无数据,页面展示零值;系统未使用模拟数据。"
} else {
res.DataDelaySeconds = res.GeneratedAt.Timestamp() - res.DataAsOf.Timestamp()
if res.DataDelaySeconds < 0 {
res.DataDelaySeconds = 0
}
if res.DataDelaySeconds > 3600 {
res.Warning = "看板汇总数据延迟超过1小时,请检查汇总任务。"
}
}
return
}
func normalizeDashboardRange(in *sysin.DashboardSummaryInp) (time.Time, time.Time) {
now := time.Now()
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
end := start
if in.StartDate != nil && in.EndDate != nil {
start = time.Date(in.StartDate.Year(), time.Month(in.StartDate.Month()), in.StartDate.Day(), 0, 0, 0, 0, now.Location())
end = time.Date(in.EndDate.Year(), time.Month(in.EndDate.Month()), in.EndDate.Day(), 0, 0, 0, 0, now.Location())
}
return start, end
}
func (s *sSysDashboard) queryRows(ctx context.Context, start, end time.Time) (rows []*entity.DashboardDailyStat, err error) {
columns := dao.DashboardDailyStat.Columns()
mod := handler.Model(dao.DashboardDailyStat.Ctx(ctx), &handler.Option{FilterTenant: true}).
WhereBetween(columns.StatDate, start.Format("2006-01-02"), end.Format("2006-01-02")).
WhereIn(columns.DimensionType, []string{dashboardDimensionGlobal, dashboardDimensionChannel}).
OrderAsc(columns.StatDate).OrderAsc(columns.Id)
if err = mod.Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "获取看板汇总数据失败")
}
return
}
func aggregateDashboardMetric(rows []*entity.DashboardDailyStat, definition dashboardMetricDefinition) int64 {
var value int64
var latestDate string
for _, row := range rows {
if row.DimensionType != dashboardDimensionGlobal || row.MetricCode != definition.Code {
continue
}
rowValue := row.MetricCount
if definition.ValueType == "amount" {
rowValue = row.MetricAmount
}
date := ""
if row.StatDate != nil {
date = row.StatDate.Format("Y-m-d")
}
if definition.Aggregation == "latest" {
if date >= latestDate {
latestDate = date
value = rowValue
}
} else {
value += rowValue
}
}
return value
}
func dashboardChangeRate(value, previous int64) float64 {
if previous == 0 {
if value == 0 {
return 0
}
return 100
}
return math.Round((float64(value-previous)/float64(previous))*10000) / 100
}
func (s *sSysDashboard) buildTrend(start, end time.Time, rows []*entity.DashboardDailyStat) []*sysin.DashboardTrendPointModel {
points := make(map[string]*sysin.DashboardTrendPointModel)
for day := start; !day.After(end); day = day.AddDate(0, 0, 1) {
date := day.Format("2006-01-02")
points[date] = &sysin.DashboardTrendPointModel{Date: date}
}
for _, row := range rows {
if row.DimensionType != dashboardDimensionGlobal || row.StatDate == nil {
continue
}
point := points[row.StatDate.Format("Y-m-d")]
if point == nil {
continue
}
switch row.MetricCode {
case "trade_order":
point.OrderAmount += row.MetricAmount
case "recharge_success":
point.RechargeAmount += row.MetricAmount
case "withdrawal_success":
point.WithdrawalAmount += row.MetricAmount
case "commission":
point.CommissionAmount += row.MetricAmount
}
}
dates := make([]string, 0, len(points))
for date := range points {
dates = append(dates, date)
}
sort.Strings(dates)
result := make([]*sysin.DashboardTrendPointModel, 0, len(dates))
for _, date := range dates {
result = append(result, points[date])
}
return result
}
func (s *sSysDashboard) buildChannels(currentRows, previousRows []*entity.DashboardDailyStat, canViewAmounts bool) []*sysin.DashboardChannelModel {
labels := map[string]string{"wechat": "微信", "alipay": "支付宝", "bank": "银行卡"}
channels := make(map[string]*sysin.DashboardChannelModel, len(labels))
for code, label := range labels {
channels[code] = &sysin.DashboardChannelModel{Channel: code, Label: label, Masked: !canViewAmounts}
}
fillChannelSummary(channels, currentRows, labels, false, !canViewAmounts)
fillChannelSummary(channels, previousRows, labels, true, !canViewAmounts)
codes := make([]string, 0, len(channels))
for code := range channels {
codes = append(codes, code)
}
sort.Strings(codes)
result := make([]*sysin.DashboardChannelModel, 0, len(codes))
for _, code := range codes {
if channels[code].Masked {
channels[code].RechargeAmount = 0
channels[code].WithdrawalAmount = 0
channels[code].PreviousRechargeAmount = 0
channels[code].PreviousWithdrawalAmount = 0
}
result = append(result, channels[code])
}
return result
}
func fillChannelSummary(channels map[string]*sysin.DashboardChannelModel, rows []*entity.DashboardDailyStat, labels map[string]string, previous, masked bool) {
for _, row := range rows {
if row.DimensionType != dashboardDimensionChannel {
continue
}
channel := channels[row.DimensionValue]
if channel == nil {
label := labels[row.DimensionValue]
if label == "" {
label = row.DimensionValue
}
channel = &sysin.DashboardChannelModel{Channel: row.DimensionValue, Label: label, Masked: masked}
channels[row.DimensionValue] = channel
}
switch row.MetricCode {
case "recharge_success":
if previous {
channel.PreviousRechargeCount += row.MetricCount
channel.PreviousRechargeAmount += row.MetricAmount
} else {
channel.RechargeCount += row.MetricCount
channel.RechargeAmount += row.MetricAmount
}
case "withdrawal_success":
if previous {
channel.PreviousWithdrawalCount += row.MetricCount
channel.PreviousWithdrawalAmount += row.MetricAmount
} else {
channel.WithdrawalCount += row.MetricCount
channel.WithdrawalAmount += row.MetricAmount
}
}
}
}
func dashboardDataStatus(rows []*entity.DashboardDailyStat) (bool, *gtime.Time) {
var latest *gtime.Time
for _, row := range rows {
if row.CalculatedAt != nil && (latest == nil || row.CalculatedAt.After(latest)) {
latest = row.CalculatedAt
}
}
return len(rows) > 0, latest
}
@@ -0,0 +1,39 @@
package sys
import (
"hotgo/internal/model/entity"
"testing"
"github.com/gogf/gf/v2/os/gtime"
)
func TestAggregateDashboardMetric(t *testing.T) {
rows := []*entity.DashboardDailyStat{
{StatDate: gtime.New("2026-10-01"), DimensionType: dashboardDimensionGlobal, MetricCode: "trade_order", MetricCount: 2, MetricAmount: 1200},
{StatDate: gtime.New("2026-10-02"), DimensionType: dashboardDimensionGlobal, MetricCode: "trade_order", MetricCount: 3, MetricAmount: 2300},
{StatDate: gtime.New("2026-10-01"), DimensionType: dashboardDimensionGlobal, MetricCode: "member_total", MetricCount: 10},
{StatDate: gtime.New("2026-10-02"), DimensionType: dashboardDimensionGlobal, MetricCode: "member_total", MetricCount: 12},
}
orderCount := aggregateDashboardMetric(rows, dashboardMetricDefinition{Code: "trade_order", ValueType: "count", Aggregation: "sum"})
if orderCount != 5 {
t.Fatalf("order count = %d, want 5", orderCount)
}
orderAmount := aggregateDashboardMetric(rows, dashboardMetricDefinition{Code: "trade_order", ValueType: "amount", Aggregation: "sum"})
if orderAmount != 3500 {
t.Fatalf("order amount = %d, want 3500", orderAmount)
}
memberTotal := aggregateDashboardMetric(rows, dashboardMetricDefinition{Code: "member_total", ValueType: "count", Aggregation: "latest"})
if memberTotal != 12 {
t.Fatalf("member total = %d, want 12", memberTotal)
}
}
func TestDashboardChangeRate(t *testing.T) {
if value := dashboardChangeRate(120, 100); value != 20 {
t.Fatalf("change rate = %v, want 20", value)
}
if value := dashboardChangeRate(0, 0); value != 0 {
t.Fatalf("zero change rate = %v, want 0", value)
}
}
+178
View File
@@ -0,0 +1,178 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"errors"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/dict"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
type sSysDictData struct{}
func NewSysDictData() *sSysDictData {
return &sSysDictData{}
}
func init() {
service.RegisterSysDictData(NewSysDictData())
}
// Delete 删除
func (s *sSysDictData) Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error {
_, err := dao.SysDictData.Ctx(ctx).Where("id", in.Id).Delete()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
return nil
}
// Edit 修改/新增
func (s *sSysDictData) Edit(ctx context.Context, in *sysin.DictDataEditInp) (err error) {
// 修改
if in.Id > 0 {
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataUpdateFields{}).WherePri(in.Id).Data(in).Update()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
return nil
}
// 新增
in.Type, err = s.GetType(ctx, in.TypeID)
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
if in.Type == "" {
return gerror.Wrap(err, "类型选择无效,请检查")
}
_, err = dao.SysDictData.Ctx(ctx).Fields(sysin.DictDataInsertFields{}).Data(in).OmitEmptyData().Insert()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
return nil
}
// List 获取列表
func (s *sSysDictData) List(ctx context.Context, in *sysin.DictDataListInp) (list []*sysin.DictDataListModel, totalCount int, err error) {
mod := dao.SysDictData.Ctx(ctx)
// 类型ID
if in.TypeID > 0 {
types, err := s.GetTypes(ctx, in.TypeID)
if err != nil {
return list, totalCount, err
}
mod = mod.WhereIn("type", types)
}
if in.Type != "" {
mod = mod.Where("type", in.Type)
}
if in.Value != "" {
mod = mod.Where("value", in.Value)
}
if in.Label != "" {
mod = mod.WhereLike("label", "%"+in.Label+"%")
}
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("sort asc,id desc").Scan(&list); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return list, totalCount, err
}
for _, v := range list {
v.TypeID, _ = s.GetId(ctx, v.Type)
}
return list, totalCount, err
}
// GetId 获取指定类型的ID
func (s *sSysDictData) GetId(ctx context.Context, t string) (id int64, err error) {
m := dao.SysDictType.Ctx(ctx).Fields(dao.SysDictType.Columns().Id).Where(dao.SysDictType.Columns().Type, t).Where(dao.SysDictType.Columns().Status, consts.StatusEnabled)
val, err := m.Value()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return 0, err
}
return val.Int64(), nil
}
// GetType 获取指定ID的类型标识
func (s *sSysDictData) GetType(ctx context.Context, id int64) (types string, err error) {
m := dao.SysDictType.Ctx(ctx).Fields(dao.SysDictType.Columns().Type).Where(dao.SysDictType.Columns().Id, id).Where(dao.SysDictType.Columns().Status, consts.StatusEnabled)
val, err := m.Value()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return types, err
}
return val.String(), nil
}
// GetTypes 获取指定ID的所有类型标识,包含下级
func (s *sSysDictData) GetTypes(ctx context.Context, id int64) (types []string, err error) {
columns, err := dao.SysDictType.Ctx(ctx).Fields(dao.SysDictType.Columns().Type).
Where(dao.SysDictType.Columns().Id, id).WhereOr(dao.SysDictType.Columns().Pid, id).Where(dao.SysDictType.Columns().Status, consts.StatusEnabled).
Array()
types = g.NewVar(columns).Strings()
return
}
// Select 获取列表
func (s *sSysDictData) Select(ctx context.Context, in *sysin.DataSelectInp) (list sysin.DataSelectModel, err error) {
// 优先从内置字典中获取
options, err := dict.GetOptions(ctx, in.Type)
if err == nil {
return options, nil
}
if !errors.Is(err, dict.NotExistKeyError) {
return nil, err
}
mod := dao.SysDictData.Ctx(ctx).Where("type", in.Type)
if err = mod.Order("sort asc,id desc").Scan(&list); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
for k, v := range list {
list[k].Value = consts.ConvType(v.Value, v.ValueType)
list[k].Key = list[k].Value
}
if len(list) == 0 {
list = make(sysin.DataSelectModel, 0)
}
return
}
+204
View File
@@ -0,0 +1,204 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/dict"
"hotgo/internal/library/hgorm"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
type sSysDictType struct{}
func NewSysDictType() *sSysDictType {
return &sSysDictType{}
}
func init() {
service.RegisterSysDictType(NewSysDictType())
}
// Tree 树
func (s *sSysDictType) Tree(ctx context.Context) (list []*sysin.DictTypeTree, err error) {
var (
mod = dao.SysDictType.Ctx(ctx)
models []*entity.SysDictType
)
if err = mod.Order("sort asc,id asc").Scan(&models); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
list = s.treeList(0, models)
return
}
// Delete 删除
func (s *sSysDictType) Delete(ctx context.Context, in *sysin.DictTypeDeleteInp) (err error) {
var models *entity.SysDictType
if err = dao.SysDictType.Ctx(ctx).Where("id", in.Id).Scan(&models); err != nil {
return
}
if models == nil {
err = gerror.New("数据不存在或已删除!")
return
}
exist, err := dao.SysDictData.Ctx(ctx).Where("type", models.Type).One()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
if !exist.IsEmpty() {
err = gerror.New("请先删除该字典类型下得所有字典数据!")
return
}
pidExist, err := dao.SysDictType.Ctx(ctx).Where("pid", models.Id).One()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
if !pidExist.IsEmpty() {
err = gerror.New("请先删除该字典类型下得所有子级类型!")
return
}
_, err = dao.SysDictType.Ctx(ctx).Where("id", in.Id).Delete()
return
}
// Edit 修改/新增
func (s *sSysDictType) Edit(ctx context.Context, in *sysin.DictTypeEditInp) (err error) {
if err = hgorm.IsUnique(ctx, &dao.SysDictType, g.Map{dao.SysDictType.Columns().Name: in.Name}, "名称已存在", in.Id); err != nil {
return
}
// 修改
if in.Id > 0 {
if _, err = dao.SysDictType.Ctx(ctx).Fields(sysin.DictTypeUpdateFields{}).WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
}
return
}
// 新增
if _, err = dao.SysDictType.Ctx(ctx).Fields(sysin.DictTypeInsertFields{}).Data(in).OmitEmptyData().Insert(); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
}
return
}
// TreeSelect 获取类型关系树选项
func (s *sSysDictType) TreeSelect(ctx context.Context, in *sysin.DictTreeSelectInp) (list []*sysin.DictTypeTree, err error) {
var (
mod = dao.SysDictType.Ctx(ctx)
models []*entity.SysDictType
)
if err = mod.Order("pid asc,sort asc").Scan(&models); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
list = s.treeList(0, models)
list = append(list, s.BuiltinSelect()...)
return
}
// BuiltinSelect 内置字典选项
func (s *sSysDictType) BuiltinSelect() (list []*sysin.DictTypeTree) {
top := &sysin.DictTypeTree{
SysDictType: entity.SysDictType{
Id: dict.BuiltinId,
Pid: 0,
},
Label: "内置字典",
Value: dict.BuiltinId,
Key: dict.BuiltinId,
}
enums := &sysin.DictTypeTree{
SysDictType: entity.SysDictType{
Id: dict.EnumsId,
Pid: dict.BuiltinId,
},
Label: "枚举字典",
Value: dict.EnumsId,
Key: dict.EnumsId,
}
for _, v := range dict.GetAllEnums() {
children := &sysin.DictTypeTree{
SysDictType: entity.SysDictType{
Id: v.Id,
Pid: dict.EnumsId,
},
Label: v.Label,
Value: v.Id,
Key: v.Id,
}
enums.Children = append(enums.Children, children)
}
fun := &sysin.DictTypeTree{
SysDictType: entity.SysDictType{
Id: dict.FuncId,
Pid: dict.BuiltinId,
},
Label: "方法字典",
Value: dict.FuncId,
Key: dict.FuncId,
}
for _, v := range dict.GetAllFunc() {
children := &sysin.DictTypeTree{
SysDictType: entity.SysDictType{
Id: v.Id,
Pid: dict.FuncId,
},
Label: v.Label,
Value: v.Id,
Key: v.Id,
}
fun.Children = append(fun.Children, children)
}
top.Children = append(top.Children, enums, fun)
list = append(list, top)
return
}
// treeList 树状列表
func (s *sSysDictType) treeList(pid int64, nodes []*entity.SysDictType) (list []*sysin.DictTypeTree) {
list = make([]*sysin.DictTypeTree, 0)
for _, v := range nodes {
if v.Pid == pid {
item := new(sysin.DictTypeTree)
item.SysDictType = *v
item.Label = v.Name
item.Value = v.Id
item.Key = v.Id
child := s.treeList(v.Id, nodes)
if len(child) > 0 {
item.Children = child
}
list = append(list, item)
}
}
return
}
+430
View File
@@ -0,0 +1,430 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"fmt"
"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/simple"
"hotgo/utility/useragent"
"hotgo/utility/validate"
"time"
"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"
)
type sSysEmsLog struct{}
func NewSysEmsLog() *sSysEmsLog {
return &sSysEmsLog{}
}
func init() {
service.RegisterSysEmsLog(NewSysEmsLog())
}
// Delete 删除
func (s *sSysEmsLog) Delete(ctx context.Context, in *sysin.EmsLogDeleteInp) (err error) {
_, err = dao.SysEmsLog.Ctx(ctx).Where("id", in.Id).Delete()
return
}
// Edit 修改/新增
func (s *sSysEmsLog) Edit(ctx context.Context, in *sysin.EmsLogEditInp) (err error) {
if in.Ip == "" {
err = gerror.New("ip不能为空")
return
}
// 修改
if in.Id > 0 {
_, err = dao.SysEmsLog.Ctx(ctx).Where("id", in.Id).Data(in).Update()
return
}
// 新增
_, err = dao.SysEmsLog.Ctx(ctx).Data(in).OmitEmptyData().Insert()
return
}
// Status 更新部门状态
func (s *sSysEmsLog) Status(ctx context.Context, in *sysin.EmsLogStatusInp) (err error) {
if in.Id <= 0 {
err = gerror.New("ID不能为空")
return
}
if in.Status <= 0 {
err = gerror.New("状态不能为空")
return
}
if !validate.InSlice(consts.StatusSlice, in.Status) {
err = gerror.New("状态不正确")
return
}
// 修改
_, err = dao.SysEmsLog.Ctx(ctx).Where("id", in.Id).Data("status", in.Status).Update()
return
}
// View 获取指定字典类型信息
func (s *sSysEmsLog) View(ctx context.Context, in *sysin.EmsLogViewInp) (res *sysin.EmsLogViewModel, err error) {
err = dao.SysEmsLog.Ctx(ctx).Where("id", in.Id).Scan(&res)
return
}
// 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
}
if totalCount == 0 {
return
}
err = mod.Page(in.Page, in.PerPage).Order("id desc").Scan(&list)
return
}
// 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).
OrderDesc(dao.SysEmsLog.Columns().Id).
Scan(&models); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
config, err := service.SysConfig().GetSmtp(ctx)
if err != nil {
return
}
in.Template, err = s.GetTemplate(ctx, in.Event, config)
if err != nil {
return
}
if err = s.AllowSend(ctx, models, config); err != nil {
return
}
if consts.IsCodeEmsTemplate(in.Event) && in.Code == "" {
in.Code = grand.Digits(4)
}
view, err := s.newView(ctx, in, config)
if err != nil {
return
}
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 = simple.AppName(ctx)
}
err = ems.Send(config, in.Email, subject, in.Content)
if err != nil {
return
}
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.CodeStatusNotUsed
data.CreatedAt = gtime.Now()
data.UpdatedAt = gtime.Now()
_, err = dao.SysEmsLog.Ctx(ctx).Data(data).OmitEmptyData().Insert()
return
}
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 = simple.AppName(ctx)
basic.Domain = "https://hotgo.facms.cn"
basic.Logo = "https://gmycos.facms.cn/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 = dao.AdminMember.Ctx(ctx).Where(dao.AdminMember.Columns().Id, user.Id).Data(g.Map{dao.AdminMember.Columns().PasswordResetToken: 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 == "" {
err = gerror.New("模板不能为空")
return
}
if config == nil {
config, err = service.SysConfig().GetSmtp(ctx)
if err != nil {
return
}
}
if len(config.Template) == 0 {
err = gerror.New("管理员还没有配置任何模板!")
return
}
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
}
// 富文本事件不限制
if models.Event == consts.EmsTemplateText {
return
}
if config == nil {
config, err = service.SysConfig().GetSmtp(ctx)
if err != nil {
return
}
}
if gtime.Now().Before(models.CreatedAt.Add(time.Second * time.Duration(config.MinInterval))) {
err = gerror.New("发送频繁,请稍后再试!")
return
}
if config.MaxIpLimit > 0 {
count, err := s.NowDayIpSendCount(ctx, models.Event)
if err != nil {
return err
}
if count >= config.MaxIpLimit {
err = gerror.New("今天发送短信过多,请次日后再试!")
return err
}
}
return
}
// NowDayIpSendCount 当天IP累计发送次数
func (s *sSysEmsLog) NowDayIpSendCount(ctx context.Context, event string) (count int, err error) {
return dao.SysEmsLog.Ctx(ctx).
Where("ip", location.GetClientIp(ghttp.RequestFromCtx(ctx))).
Where("event", event).
WhereGTE("created_at", gtime.Now().Format("Y-m-d")).
Count()
}
// VerifyCode 效验验证码
func (s *sSysEmsLog) VerifyCode(ctx context.Context, in *sysin.VerifyEmsCodeInp) (err error) {
if in.Event == "" {
err = gerror.New("事件不能为空")
return
}
if in.Email == "" {
err = gerror.New("邮箱不能为空")
return
}
if in.Event == consts.EmsTemplateResetPwd || in.Event == consts.EmsTemplateText {
err = gerror.Newf("事件类型无需验证:%v", in.Event)
return
}
config, err := service.SysConfig().GetSmtp(ctx)
if err != nil {
return
}
var models *entity.SysEmsLog
if err = dao.SysEmsLog.Ctx(ctx).
Where(dao.SysEmsLog.Columns().Event, in.Event).
Where(dao.SysEmsLog.Columns().Email, in.Email).
OrderDesc(dao.SysEmsLog.Columns().Id).
Scan(&models); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
if models == nil {
err = gerror.New("验证码错误")
return
}
if models.Times >= 10 {
err = gerror.New("验证码错误次数过多,请重新发送!")
return
}
if in.Event != consts.EmsTemplateCode {
if models.Status == consts.CodeStatusUsed {
err = gerror.New("验证码已使用,请重新发送!")
return
}
}
if gtime.Now().After(models.CreatedAt.Add(time.Second * time.Duration(config.CodeExpire))) {
err = gerror.New("验证码已过期,请重新发送")
return
}
if models.Code != in.Code {
_, _ = dao.SysEmsLog.Ctx(ctx).Where(dao.SysEmsLog.Columns().Id, models.Id).Increment(dao.SysEmsLog.Columns().Times, 1)
err = gerror.New("验证码错误!")
return
}
_, err = dao.SysEmsLog.Ctx(ctx).Where(dao.SysEmsLog.Columns().Id, models.Id).Data(g.Map{
dao.SysEmsLog.Columns().Times: models.Times + 1,
dao.SysEmsLog.Columns().Status: consts.CodeStatusUsed,
dao.SysEmsLog.Columns().UpdatedAt: gtime.Now(),
}).Update()
return
}
+351
View File
@@ -0,0 +1,351 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/hggen"
"hotgo/internal/model"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/validate"
"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/os/gtime"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
)
type sSysGenCodes struct{}
func NewSysGenCodes() *sSysGenCodes {
return &sSysGenCodes{}
}
func init() {
service.RegisterSysGenCodes(NewSysGenCodes())
}
// Delete 删除
func (s *sSysGenCodes) Delete(ctx context.Context, in *sysin.GenCodesDeleteInp) (err error) {
_, err = dao.SysGenCodes.Ctx(ctx).Where("id", in.Id).Delete()
return
}
// Edit 修改/新增
func (s *sSysGenCodes) Edit(ctx context.Context, in *sysin.GenCodesEditInp) (res *sysin.GenCodesEditModel, err error) {
if in.GenType == 0 {
err = gerror.New("生成类型不能为空")
return
}
if in.VarName == "" {
err = gerror.New("实体名称不能为空")
return
}
if in.GenType == consts.GenCodesTypeCurd {
var temp *model.GenerateAppCrudTemplate
cfg := fmt.Sprintf("hggen.application.crud.templates.%v", in.GenTemplate)
if err = g.Cfg().MustGet(ctx, cfg).Scan(&temp); err != nil {
return
}
if temp == nil {
err = gerror.Newf("选择的模板不存在:%v", cfg)
return
}
if temp.IsAddon && in.AddonName == "" {
err = gerror.New("插件模板必须选择一个有效的插件")
return
}
}
// 修改
in.UpdatedAt = gtime.Now()
if in.Id > 0 {
_, err = dao.SysGenCodes.Ctx(ctx).Where("id", in.Id).Data(in).Update()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
return &sysin.GenCodesEditModel{SysGenCodes: in.SysGenCodes}, nil
}
// 新增
if in.Options.IsNil() {
in.Options = gjson.New(consts.NilJsonToString)
}
in.MasterColumns = gjson.New("[]")
in.Status = consts.GenCodesStatusWait
in.CreatedAt = gtime.Now()
id, err := dao.SysGenCodes.Ctx(ctx).Data(in).OmitEmptyData().InsertAndGetId()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
in.Id = id
return &sysin.GenCodesEditModel{SysGenCodes: in.SysGenCodes}, nil
}
// Status 更新部门状态
func (s *sSysGenCodes) Status(ctx context.Context, in *sysin.GenCodesStatusInp) (err error) {
if in.Id <= 0 {
err = gerror.New("ID不能为空")
return
}
if in.Status <= 0 {
err = gerror.New("状态不能为空")
return
}
if !validate.InSlice(consts.StatusSlice, in.Status) {
err = gerror.New("状态不正确")
return
}
_, err = dao.SysGenCodes.Ctx(ctx).Where("id", in.Id).Data("status", in.Status).Update()
return
}
// MaxSort 最大排序
func (s *sSysGenCodes) MaxSort(ctx context.Context, in *sysin.GenCodesMaxSortInp) (res *sysin.GenCodesMaxSortModel, err error) {
if in.Id > 0 {
if err = dao.SysGenCodes.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.GenCodesMaxSortModel)
}
res.Sort = form.DefaultMaxSort(res.Sort)
return
}
// View 获取指定字典类型信息
func (s *sSysGenCodes) View(ctx context.Context, in *sysin.GenCodesViewInp) (res *sysin.GenCodesViewModel, err error) {
err = dao.SysGenCodes.Ctx(ctx).Where("id", in.Id).Scan(&res)
return
}
// List 获取列表
func (s *sSysGenCodes) List(ctx context.Context, in *sysin.GenCodesListInp) (list []*sysin.GenCodesListModel, totalCount int, err error) {
mod := dao.SysGenCodes.Ctx(ctx)
if in.GenType > 0 {
mod = mod.Where("gen_type", in.GenType)
}
if in.VarName != "" {
mod = mod.Where("var_name", in.VarName)
}
if in.Status > 0 {
mod = mod.Where("status", in.Status)
}
totalCount, err = mod.Count()
if err != nil || totalCount == 0 {
return
}
if err = mod.Page(in.Page, in.PerPage).Order("id desc").Scan(&list); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
typeSelect, err := hggen.GenTypeSelect(ctx)
if err != nil {
return
}
getTpGroup := func(row *sysin.GenCodesListModel) string {
if row == nil {
return ""
}
genType := int(row.GenType)
if genType == consts.GenCodesTypeTree {
genType = consts.GenCodesTypeCurd
}
for _, v := range typeSelect {
if v.Value == genType {
for index, template := range v.Templates {
if index == row.GenTemplate {
return template.Label
}
}
}
}
return ""
}
if len(list) > 0 {
for _, v := range list {
v.GenTemplateGroup = getTpGroup(v)
}
}
return
}
// Selects 选项
func (s *sSysGenCodes) Selects(ctx context.Context, in *sysin.GenCodesSelectsInp) (res *sysin.GenCodesSelectsModel, err error) {
return hggen.TableSelects(ctx, in)
}
// TableSelect 表选项
func (s *sSysGenCodes) TableSelect(ctx context.Context, in *sysin.GenCodesTableSelectInp) (res []*sysin.GenCodesTableSelectModel, err error) {
var (
sql string
config = g.DB(in.Name).GetConfig()
disableTables = g.Cfg().MustGet(ctx, "hggen.disableTables").Strings()
lists []*sysin.GenCodesTableSelectModel
)
// 根据数据库类型使用不同的SQL
if config.Type == consts.DBPgsql {
// PostgreSQL: 使用pg_catalog查询表和注释
sql = `
SELECT
c.relname as value,
COALESCE(obj_description(c.oid), '') as label
FROM pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE n.nspname = 'public'
AND c.relkind = 'r'
ORDER BY c.relname`
} else {
// MySQL: 使用information_schema.TABLES
sql = fmt.Sprintf("SELECT TABLE_NAME as value, TABLE_COMMENT as label FROM information_schema.TABLES WHERE TABLE_SCHEMA = '%s'", config.Name)
}
if err = g.DB(in.Name).Ctx(ctx).Raw(sql).Scan(&lists); err != nil {
return
}
patternStr := `addon_(\w+)_`
repStr := ``
for _, v := range lists {
if gstr.InArray(disableTables, v.Value) {
continue
}
newValue := v.Value
if config.Prefix != "" {
newValue = gstr.SubStrFromEx(v.Value, config.Prefix)
}
if newValue == "" {
err = gerror.Newf("表名[%v]前缀必须和配置中的前缀设置[%v] 保持一致", v.Value, config.Prefix)
return
}
// 如果是插件模块,则移除掉插件表前缀
bt, err := gregex.Replace(patternStr, []byte(repStr), []byte(newValue))
if err != nil {
err = gerror.Newf("表名[%v] gregex.Replace err:%v", v.Value, err.Error())
return nil, err
}
row := v
row.DefTableComment = v.Label
row.DaoName = gstr.CaseCamel(newValue)
row.DefVarName = gstr.CaseCamel(string(bt))
row.DefAlias = gstr.CaseCamelLower(newValue)
row.Name = fmt.Sprintf("%s (%s)", v.Value, v.Label)
row.Label = row.Name
res = append(res, row)
}
return
}
// ColumnSelect 表字段选项
func (s *sSysGenCodes) ColumnSelect(ctx context.Context, in *sysin.GenCodesColumnSelectInp) (res []*sysin.GenCodesColumnSelectModel, err error) {
var (
sql string
config = g.DB(in.Name).GetConfig()
)
// 根据数据库类型使用不同的SQL
if config.Type == consts.DBPgsql {
// PostgreSQL: 使用pg_catalog查询列注释
sql = `
SELECT
a.attname as value,
COALESCE(col_description(a.attrelid, a.attnum), '') as label
FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE n.nspname = 'public'
AND c.relname = '%s'
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY a.attnum`
sql = fmt.Sprintf(sql, in.Table)
} else {
// MySQL: 使用information_schema.COLUMNS
sql = fmt.Sprintf("SELECT COLUMN_NAME as value, COLUMN_COMMENT as label FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'", config.Name, in.Table)
}
if err = g.DB(in.Name).Ctx(ctx).Raw(sql).Scan(&res); err != nil {
return
}
if len(res) == 0 {
err = gerror.Newf("table does not exist:%v", in.Table)
return
}
for k, v := range res {
res[k].Name = fmt.Sprintf("%s (%s)", v.Value, v.Label)
res[k].Label = res[k].Name
}
return
}
// ColumnList 表字段列表
func (s *sSysGenCodes) ColumnList(ctx context.Context, in *sysin.GenCodesColumnListInp) (res []*sysin.GenCodesColumnListModel, err error) {
return hggen.TableColumns(ctx, in)
}
// Preview 生成预览
func (s *sSysGenCodes) Preview(ctx context.Context, in *sysin.GenCodesPreviewInp) (res *sysin.GenCodesPreviewModel, err error) {
return hggen.Preview(ctx, in)
}
// Build 提交生成
func (s *sSysGenCodes) Build(ctx context.Context, in *sysin.GenCodesBuildInp) (err error) {
// 先保存配置
ein := in.SysGenCodes
if _, err = s.Edit(ctx, &sysin.GenCodesEditInp{SysGenCodes: ein}); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
if err = s.Status(ctx, &sysin.GenCodesStatusInp{Id: in.Id, Status: consts.GenCodesStatusOk}); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
if err = hggen.Build(ctx, in); err != nil {
_ = s.Status(ctx, &sysin.GenCodesStatusInp{Id: in.Id, Status: consts.GenCodesStatusFail})
return err
}
return
}
+452
View File
@@ -0,0 +1,452 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/global"
"hotgo/internal/library/contexts"
"hotgo/internal/library/dict"
"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/sysin"
"hotgo/internal/service"
"hotgo/utility/excel"
"hotgo/utility/simple"
"hotgo/utility/validate"
"github.com/gogf/gf/v2/database/gdb"
"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/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/gmode"
)
type sSysLog struct{}
func NewSysLog() *sSysLog {
return &sSysLog{}
}
func init() {
service.RegisterSysLog(NewSysLog())
}
// Model 请求日志Orm模型
func (s *sSysLog) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(dao.SysLog.Ctx(ctx), option...)
}
// Export 导出
func (s *sSysLog) Export(ctx context.Context, in *sysin.LogListInp) (err error) {
// 导出格式
type exportImage struct {
Id int64 `json:"id" description:""`
AppId string `json:"app_id" description:"应用id"`
Method string `json:"method" description:"提交类型"`
Module string `json:"module" description:"模块"`
Url string `json:"url" description:"提交url"`
Ip string `json:"ip" description:"ip地址"`
ErrorCode int `json:"error_code" description:"报错code"`
ErrorMsg string `json:"error_msg" description:"报错信息"`
ReqId string `json:"req_id" description:"对外id"`
TakeUpTime int64 `json:"take_up_time" description:"请求耗时"`
CreatedAt *gtime.Time `json:"created_at" description:"创建时间"`
MemberName string `json:"memberName"`
Region string `json:"region"`
}
var (
titleList = []string{"ID", "应用", "提交类型", "模块", "提交url", "ip地址", "报错code", "报错信息", "对外id", "请求耗时", "创建时间", "用户", "访问地"}
fileName = "访问日志导出-" + gctx.CtxId(ctx)
sheetName = simple.AppName(ctx)
exportList []exportImage
row exportImage
)
list, _, err := s.List(ctx, in)
if err != nil {
return err
}
// 格式化格式
for i := 0; i < len(list); i++ {
row.Id = list[i].Id
row.AppId = list[i].AppId
row.Module = list[i].Module
row.Method = list[i].Method
row.Url = list[i].Url
row.Ip = list[i].Ip
row.ReqId = list[i].ReqId
row.ErrorCode = list[i].ErrorCode
row.ErrorMsg = list[i].ErrorMsg
row.TakeUpTime = list[i].TakeUpTime
row.CreatedAt = list[i].CreatedAt
row.MemberName = list[i].MemberName
row.Region = list[i].Region
exportList = append(exportList, row)
}
err = excel.ExportByStructs(ctx, titleList, exportList, fileName, sheetName)
return
}
// RealWrite 真实写入
func (s *sSysLog) RealWrite(ctx context.Context, log entity.SysLog) (err error) {
_, err = dao.SysLog.Ctx(ctx).FieldsEx(dao.SysLog.Columns().Id).Data(log).Unscoped().OmitEmptyData().Insert()
return
}
// AutoLog 根据配置自动记录请求日志
func (s *sSysLog) AutoLog(ctx context.Context) error {
return g.Try(ctx, func(ctx context.Context) {
var err error
defer func() {
if err != nil {
g.Log().Error(ctx, "autoLog err:%+v", err)
}
}()
config, err := service.SysConfig().GetLoadLog(ctx)
if err != nil {
return
}
if config == nil || !config.Switch {
return
}
data := s.AnalysisLog(ctx)
if ok := validate.InSliceExistStr(config.Module, data.Module); !ok {
return
}
if ok := validate.InSliceExistStr(config.SkipCode, gconv.String(data.ErrorCode)); ok {
return
}
if config.Queue {
err = queue.Push(consts.QueueLogTopic, data)
return
}
err = s.RealWrite(ctx, data)
})
}
// AnalysisLog 解析日志数据
func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
var (
mctx = contexts.Get(ctx)
response = mctx.Response
user = mctx.User
request = ghttp.RequestFromCtx(ctx)
module = mctx.Module
clientIp = location.GetClientIp(request)
postData = gjson.New(consts.NilJsonToString)
getData = gjson.New(request.URL.Query())
headerData = gjson.New(consts.NilJsonToString)
errorData = gjson.New(consts.NilJsonToString)
data entity.SysLog
memberId int64
errorCode int
errorMsg string
traceID string
timestamp int64
appId string
takeUpTime int64
)
// 响应数据
if response != nil {
errorCode = response.Code
errorMsg = response.Message
traceID = response.TraceID
timestamp = response.Timestamp
if len(gconv.String(response.Error)) > 0 {
errorData = gjson.New(response.Error)
}
}
if timestamp == 0 {
timestamp = gtime.Timestamp()
}
// 请求头
if reqHeadersBytes, _ := gjson.New(request.Header).MarshalJSON(); len(reqHeadersBytes) > 0 {
headerData = gjson.New(reqHeadersBytes)
}
// post参数
if body, ok := mctx.Data["request.body"].(*gjson.Json); ok {
postData = body
}
// post表单
postForm := gjson.New(gconv.String(request.PostForm)).Map()
if len(postForm) > 0 {
for k, v := range postForm {
postData.MustSet(k, v)
}
}
if postData.IsNil() || len(postData.Map()) == 0 {
postData = gjson.New(consts.NilJsonToString)
} else {
// 隐藏明文显示的密码
for k := range postData.Map() {
if gstr.ContainsI(k, "password") {
postData.MustSet(k, "******")
}
}
}
// 当前登录用户
if user != nil {
memberId = user.Id
appId = user.App
}
ipData, err := location.GetLocation(ctx, clientIp)
if err != nil {
g.Log().Debugf(ctx, "location.GetLocation clientIp:%v, err:%+v", clientIp, err)
}
if ipData == nil {
ipData = new(location.IpLocationData)
}
// 请求耗时
if tt, ok := mctx.Data["request.takeUpTime"].(int64); ok {
takeUpTime = tt
}
headerData.MustAppend("Enter-Time", request.EnterTime.String())
data = entity.SysLog{
AppId: appId,
MerchantId: 0,
MemberId: memberId,
Method: request.Method,
Module: module,
Url: request.URL.Path,
GetData: getData,
PostData: postData,
HeaderData: s.SimplifyHeaderParams(headerData),
Ip: clientIp,
ProvinceId: ipData.ProvinceCode,
CityId: ipData.CityCode,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
ErrorData: errorData,
ReqId: traceID,
Timestamp: timestamp,
UserAgent: request.Header.Get("User-Agent"),
Status: consts.StatusEnabled,
TakeUpTime: takeUpTime,
UpdatedAt: gtime.Now(),
CreatedAt: request.EnterTime,
}
return data
}
// SimplifyHeaderParams 过滤掉请求头中的大参数
func (s *sSysLog) SimplifyHeaderParams(data *gjson.Json) *gjson.Json {
if data == nil || data.IsNil() {
return data
}
var filters = []string{"Accept", "Authorization", "Cookie"}
for _, filter := range filters {
v := data.Get(filter)
if len(v.String()) > 128 {
data.MustRemove(filter)
}
}
return data
}
// View 获取指定请求日志信息
func (s *sSysLog) View(ctx context.Context, in *sysin.LogViewInp) (res *sysin.LogViewModel, err error) {
mod := s.Model(ctx)
count, err := service.SysLoginLog().Model(ctx).
LeftJoinOnFields(dao.SysLog.Table(), dao.SysLoginLog.Columns().ReqId, "=", dao.SysLog.Columns().ReqId).
WherePrefix(dao.SysLog.Table(), dao.SysLog.Columns().Id, in.Id).Count()
if err != nil {
return nil, err
}
if count > 0 {
mod = dao.SysLog.Ctx(ctx)
}
if err = mod.Hook(hook.CityLabel).WherePri(in.Id).Scan(&res); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
if res == nil {
return
}
routes := global.LoadHTTPRoutes(ghttp.RequestFromCtx(ctx))
key := global.GenRouteKey(res.Method, res.Url)
route, ok := routes[key]
if ok {
res.Tags = route.Tags
res.Summary = route.Summary
res.Description = route.Description
}
if simple.IsDemo(ctx) {
res.HeaderData = gjson.New(`{
"none": [
"` + consts.DemoTips + `"
]
}`)
}
return
}
// Delete 删除请求日志
func (s *sSysLog) Delete(ctx context.Context, in *sysin.LogDeleteInp) (err error) {
_, err = s.Model(ctx).WherePri(in.Id).Delete()
return
}
// List 请求日志列表
func (s *sSysLog) List(ctx context.Context, in *sysin.LogListInp) (list []*sysin.LogListModel, totalCount int, err error) {
mod := s.Model(ctx).FieldsEx("get_data", "header_data", "post_data")
// 访问路径
if in.Url != "" {
mod = mod.WhereLike("url", "%"+in.Url+"%")
}
// 模块
if in.Module != "" {
mod = mod.Where("module", in.Module)
}
// 链路ID
if in.ReqId != "" {
mod = mod.Where("req_id", in.ReqId)
}
// 请求方式
if in.Method != "" {
mod = mod.Where("method", in.Method)
}
// 用户
if in.MemberId > 0 {
mod = mod.Where("member_id", in.MemberId)
}
// 操作人筛选
if len(in.ComplexMemberId) == 2 && len(in.ComplexMemberId[0]) > 0 {
memberIds, err := service.AdminMember().GetComplexMemberIds(ctx, in.ComplexMemberId[0], in.ComplexMemberId[1])
if err != nil {
return nil, 0, err
}
if len(memberIds) == 0 {
return nil, 0, nil
}
mod = mod.WhereIn("member_id", memberIds)
}
// 访问IP
if in.Ip != "" {
mod = mod.Where("ip", in.Ip)
}
// 日期范围
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween("created_at", gtime.New(in.CreatedAt[0]), gtime.New(in.CreatedAt[1]))
}
// 状态码
if in.ErrorCode != "" {
mod = mod.Where("error_code", in.ErrorCode)
}
// 请求耗时
if dict.HasOptionKey(consts.HTTPHandlerTimeOptions, in.TakeUpTime) {
mod = mod.Where(gdb.Raw(fmt.Sprintf("(`take_up_time` %v)", in.TakeUpTime)))
}
// 非生产环境,允许关键词查询日志
// 生成环境使用需谨慎,日志量大易产生慢日志
if !gmode.IsProduct() && in.Keyword != "" {
filterColumns := map[string]string{
"get_data": "LIKE",
"post_data": "LIKE",
"header_data": "LIKE",
"error_data": "LIKE",
"error_msg": "LIKE",
}
mod = hgorm.FilterKeywordsWithOr(mod, filterColumns, in.Keyword)
}
totalCount, err = mod.Count()
if err != nil || totalCount == 0 {
return
}
if err = mod.Page(in.Page, in.PerPage).Hook(hook.CityLabel).Order("id desc").Scan(&list); err != nil {
return
}
routes := global.LoadHTTPRoutes(ghttp.RequestFromCtx(ctx))
for _, v := range list {
if v.AppId == consts.AppAdmin {
memberName, err := dao.AdminMember.Ctx(ctx).Fields(dao.AdminMember.Columns().RealName).WherePri(v.MemberId).Value()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return list, totalCount, err
}
v.MemberName = memberName.String()
}
if v.MemberName == "" {
v.MemberName = "游客"
}
// 截取请求url路径
if gstr.Contains(v.Url, "?") {
v.Url = gstr.StrTillEx(v.Url, "?")
}
key := global.GenRouteKey(v.Method, v.Url)
route, ok := routes[key]
if ok {
v.Tags = route.Tags
v.Summary = route.Summary
v.Description = route.Description
}
if simple.IsDemo(ctx) {
v.HeaderData = gjson.New(`{
"none": [
"` + consts.DemoTips + `"
]
}`)
}
}
return
}
+183
View File
@@ -0,0 +1,183 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 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/database/gdb"
"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/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/library/location"
"hotgo/internal/library/queue"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/adminin"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/convert"
"hotgo/utility/excel"
"hotgo/utility/useragent"
)
type sSysLoginLog struct{}
func NewSysLoginLog() *sSysLoginLog {
return &sSysLoginLog{}
}
func init() {
service.RegisterSysLoginLog(NewSysLoginLog())
}
// Model 登录日志Orm模型
func (s *sSysLoginLog) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(dao.SysLoginLog.Ctx(ctx), option...)
}
// List 获取登录日志列表
func (s *sSysLoginLog) List(ctx context.Context, in *sysin.LoginLogListInp) (list []*sysin.LoginLogListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 查询状态
if in.Status > 0 {
mod = mod.Where(dao.SysLoginLog.Columns().Status, in.Status)
}
// 查询登录时间
if len(in.LoginAt) == 2 {
mod = mod.WhereBetween(dao.SysLoginLog.Columns().LoginAt, in.LoginAt[0], in.LoginAt[1])
}
// 查询IP地址
if in.LoginIp != "" {
mod = mod.Where(dao.SysLoginLog.Columns().LoginIp, in.LoginIp)
}
// 用户名
if in.Username != "" {
mod = mod.Where(dao.SysLoginLog.Columns().Username, in.Username)
}
totalCount, err = mod.Clone().Count()
if err != nil || totalCount == 0 {
return
}
if err = mod.Fields(sysin.LoginLogListModel{}).Hook(hook.CityLabel).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 {
if v.Response.Contains("token") {
v.Response.Set("token", "******")
}
v.Os = useragent.GetOs(v.UserAgent)
v.Browser = useragent.GetBrowser(v.UserAgent)
v.SysLogId, err = dao.SysLog.Ctx(ctx).Fields(dao.SysLog.Columns().Id).Where(dao.SysLog.Columns().ReqId, v.ReqId).Value()
if err != nil {
return nil, 0, err
}
}
return
}
// Export 导出登录日志
func (s *sSysLoginLog) Export(ctx context.Context, in *sysin.LoginLogListInp) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(sysin.LoginLogExportModel{})
if err != nil {
return
}
var (
fileName = "导出登录日志-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, form.CalPageCount(totalCount, in.PerPage), in.Page, len(list))
exports []sysin.LoginLogExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Delete 删除登录日志
func (s *sSysLoginLog) Delete(ctx context.Context, in *sysin.LoginLogDeleteInp) (err error) {
_, err = s.Model(ctx).WherePri(in.Id).Delete()
return
}
// Push 推送登录日志
func (s *sSysLoginLog) Push(ctx context.Context, in *sysin.LoginLogPushInp) {
if in.Response == nil {
in.Response = new(adminin.LoginModel)
}
r := ghttp.RequestFromCtx(ctx)
if r == nil {
g.Log().Warningf(ctx, "ctx not http request")
return
}
clientIp := location.GetClientIp(r)
ipData, err := location.GetLocation(ctx, clientIp)
if err != nil {
g.Log().Debugf(ctx, "location.GetLocation clientIp:%v, err:%+v", clientIp, err)
}
if ipData == nil {
ipData = new(location.IpLocationData)
}
var models entity.SysLoginLog
models.ReqId = gctx.CtxId(ctx)
models.MemberId = in.Response.Id
models.Username = in.Response.Username
models.LoginAt = gtime.Now()
models.LoginIp = clientIp
models.UserAgent = r.UserAgent()
models.ProvinceId = ipData.ProvinceCode
models.CityId = ipData.CityCode
models.Status = consts.StatusEnabled
if in.Err != nil {
models.Status = consts.StatusDisable
models.ErrMsg = in.Err.Error()
}
models.Response = gjson.New(consts.NilJsonToString)
if in.Response != nil {
models.Response = gjson.New(in.Response)
}
if err = queue.Push(consts.QueueLoginLogTopic, models); err != nil {
g.Log().Warningf(ctx, "push LoginLog err:%+v, models:%v", err, gjson.New(models).String())
}
}
// RealWrite 真实写入
func (s *sSysLoginLog) RealWrite(ctx context.Context, models entity.SysLoginLog) (err error) {
_, err = dao.SysLoginLog.Ctx(ctx).Data(models).OmitEmptyData().Insert()
return
}
+310
View File
@@ -0,0 +1,310 @@
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/dict"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"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/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysMemberLevel struct{}
func NewSysMemberLevel() *sSysMemberLevel { return &sSysMemberLevel{} }
func init() {
service.RegisterSysMemberLevel(NewSysMemberLevel())
dict.RegisterFunc("memberLevelOption", "会员等级选项", service.SysMemberLevel().Option, true)
}
func (s *sSysMemberLevel) model(ctx context.Context) *gdb.Model {
return handler.Model(dao.AppMemberLevel.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysMemberLevel) versionModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.AppMemberLevelVersion.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysMemberLevel) List(ctx context.Context, in *sysin.MemberLevelListInp) (list []*sysin.MemberLevelListModel, total int, err error) {
lc, vc := dao.AppMemberLevel.Columns(), dao.AppMemberLevelVersion.Columns()
// 已发布等级展示当前生效版本;尚未发布时展示最新草稿,避免列表误显示为零值。
versionJoin := fmt.Sprintf(
"%s=COALESCE(NULLIF(%s,0),(SELECT lv.%s FROM %s lv WHERE lv.%s=%s ORDER BY lv.%s DESC,lv.%s DESC LIMIT 1))",
col("v", vc.Id), col("l", lc.CurrentVersionId), vc.Id, dao.AppMemberLevelVersion.Table(), vc.LevelId, col("l", lc.Id), vc.VersionNo, vc.Id,
)
mod := s.model(ctx).As("l").LeftJoin(dao.AppMemberLevelVersion.Table()+" v", versionJoin).Fields(col("l", lc.Id), col("l", lc.LevelCode), col("l", lc.Name), col("l", lc.IconUrl), col("l", lc.CurrentVersionId), col("l", lc.MemberCount), col("l", lc.Status), col("l", lc.Sort), col("l", lc.Remark), col("l", lc.CreatedAt), col("v", vc.VersionNo), col("v", vc.UpgradeAmount), col("v", vc.GiftAmount), col("v", vc.DailyOrderLimit), col("v", vc.CommissionRate), col("v", vc.DailyWithdrawLimit), col("v", vc.PublishStatus), col("v", vc.EffectiveAt))
if in.Name != "" {
mod = mod.WhereLike(col("l", lc.Name), "%"+in.Name+"%")
}
if in.Status > 0 {
mod = mod.Where(col("l", lc.Status), in.Status)
}
if in.PublishStatus > 0 {
mod = mod.Where(col("v", vc.PublishStatus), in.PublishStatus)
}
err = mod.Page(in.Page, in.PerPage).OrderAsc(col("l", lc.Sort)).OrderDesc(col("l", lc.Id)).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取会员等级列表失败")
}
return
}
func (s *sSysMemberLevel) View(ctx context.Context, in *sysin.MemberLevelViewInp) (res *sysin.MemberLevelViewModel, err error) {
res = new(sysin.MemberLevelViewModel)
if err = s.model(ctx).WherePri(in.Id).Scan(&res.Level); err != nil {
return nil, gerror.Wrap(err, "获取会员等级失败")
}
if res.Level == nil {
return nil, gerror.New("会员等级不存在")
}
vc := dao.AppMemberLevelVersion.Columns()
if err = s.versionModel(ctx).Where(vc.LevelId, in.Id).WhereIn(vc.PublishStatus, []int{consts.BizPublishDraft, consts.BizPublishPending}).OrderDesc(vc.Id).Limit(1).Scan(&res.Draft); err != nil {
return nil, err
}
if res.Level.CurrentVersionId > 0 {
if err = s.versionModel(ctx).WherePri(res.Level.CurrentVersionId).Scan(&res.Published); err != nil {
return nil, err
}
}
return
}
func (s *sSysMemberLevel) Save(ctx context.Context, in *sysin.MemberLevelSaveInp) (id, versionId uint64, err error) {
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
lc, vc := dao.AppMemberLevel.Columns(), dao.AppMemberLevelVersion.Columns()
count, e := s.model(ctx).Where(lc.LevelCode, in.LevelCode).WhereNot(lc.Id, in.Id).Count()
if e != nil {
return e
}
if count > 0 {
return gerror.New("等级编码已存在")
}
count, e = s.model(ctx).Where(lc.Name, in.Name).WhereNot(lc.Id, in.Id).Count()
if e != nil {
return e
}
if count > 0 {
return gerror.New("等级名称已存在")
}
userId := contexts.GetUserId(ctx)
if in.Id == 0 {
result, e := s.model(ctx).Fields(lc.LevelCode, lc.Name, lc.IconAttachmentId, lc.IconUrl, lc.Status, lc.Sort, lc.Remark, lc.CreatedBy, lc.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{lc.LevelCode: in.LevelCode, lc.Name: in.Name, lc.IconAttachmentId: in.IconAttachmentId, lc.IconUrl: in.IconUrl, lc.Status: in.Status, lc.Sort: in.Sort, lc.Remark: in.Remark, lc.CreatedBy: userId, lc.UpdatedBy: userId}).Insert()
if e != nil {
return gerror.Wrap(e, "创建会员等级失败")
}
last, _ := result.LastInsertId()
in.Id = uint64(last)
} else {
_, e = s.model(ctx).WherePri(in.Id).Data(g.Map{lc.LevelCode: in.LevelCode, lc.Name: in.Name, lc.IconAttachmentId: in.IconAttachmentId, lc.IconUrl: in.IconUrl, lc.Status: in.Status, lc.Sort: in.Sort, lc.Remark: in.Remark, lc.UpdatedBy: userId}).Update()
if e != nil {
return gerror.Wrap(e, "更新会员等级失败")
}
}
id = in.Id
data := g.Map{vc.UpgradeAmount: in.UpgradeAmount, vc.GiftAmount: in.GiftAmount, vc.DailyOrderLimit: in.DailyOrderLimit, vc.CommissionRate: in.CommissionRate, vc.DailyWithdrawLimit: in.DailyWithdrawLimit, vc.RuleJson: in.RuleJson, vc.PublishStatus: consts.BizPublishPublished, vc.EffectiveAt: nil, vc.PublishedAt: gtime.Now(), vc.Remark: in.Remark, vc.UpdatedBy: userId}
var level *entity.AppMemberLevel
if e = s.model(ctx).WherePri(in.Id).Scan(&level); e != nil {
return e
}
if level == nil {
return gerror.New("会员等级不存在")
}
versionId = level.CurrentVersionId
if versionId == 0 {
var latest *entity.AppMemberLevelVersion
if e = s.versionModel(ctx).Where(vc.LevelId, in.Id).OrderDesc(vc.Id).Limit(1).Scan(&latest); e != nil {
return e
}
if latest != nil {
versionId = latest.Id
}
}
if versionId > 0 {
data[vc.Version] = gdb.Raw(vc.Version + "+1")
if _, e = s.versionModel(ctx).WherePri(versionId).Where(vc.LevelId, in.Id).Data(data).Update(); e != nil {
return gerror.Wrap(e, "更新等级规则失败")
}
} else {
data[vc.LevelId] = in.Id
data[vc.VersionNo] = 1
data[vc.CreatedBy] = userId
result, insertErr := s.versionModel(ctx).Fields(vc.LevelId, vc.VersionNo, vc.UpgradeAmount, vc.GiftAmount, vc.DailyOrderLimit, vc.CommissionRate, vc.DailyWithdrawLimit, vc.RuleJson, vc.PublishStatus, vc.EffectiveAt, vc.PublishedAt, vc.Remark, vc.CreatedBy, vc.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
if insertErr != nil {
return gerror.Wrap(insertErr, "创建等级规则失败")
}
last, _ := result.LastInsertId()
versionId = uint64(last)
}
if _, e = s.model(ctx).WherePri(in.Id).Data(g.Map{lc.CurrentVersionId: versionId, lc.UpdatedBy: userId}).Update(); e != nil {
return gerror.Wrap(e, "更新等级当前规则失败")
}
_ = dict.ClearFuncCache("memberLevelOption")
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member_level", Action: "level.save", TargetType: "app_member_level", TargetId: in.Id, After: g.Map{"upgradeAmount": in.UpgradeAmount, "giftAmount": in.GiftAmount, "commissionRate": in.CommissionRate}, Reason: in.Remark})
})
return
}
func (s *sSysMemberLevel) Submit(ctx context.Context, in *sysin.MemberLevelSubmitInp) (res *sysin.MemberLevelApprovalModel, err error) {
var v *entity.AppMemberLevelVersion
vc := dao.AppMemberLevelVersion.Columns()
if err = s.versionModel(ctx).WherePri(in.VersionId).Scan(&v); err != nil {
return
}
if v == nil {
return nil, gerror.New("等级规则版本不存在")
}
if v.PublishStatus != consts.BizPublishDraft {
return nil, gerror.New("仅草稿可以提交审批")
}
if v.Version != in.Version {
return nil, gerror.New("等级规则已更新,请刷新后重试")
}
var level *entity.AppMemberLevel
_ = s.model(ctx).WherePri(v.LevelId).Scan(&level)
approval, err := service.SysApproval().Create(ctx, &sysin.ApprovalCreateInp{ApprovalType: "member_level_publish", TargetType: "app_member_level_version", TargetId: v.Id, Action: "publish", Title: "发布会员等级 " + level.Name + fmt.Sprintf(" V%d", v.VersionNo), PayloadJson: gjson.New(v), RiskLevel: consts.ApprovalRiskHigh, RequiredApprovals: 1, Remark: in.Reason})
if err != nil {
return nil, err
}
if err = service.SysApproval().Submit(ctx, &sysin.ApprovalSubmitInp{Id: approval.Id, Version: 0}); err != nil {
return nil, err
}
result, err := s.versionModel(ctx).WherePri(v.Id).Where(vc.Version, v.Version).Data(g.Map{vc.PublishStatus: consts.BizPublishPending, vc.ApprovalRequestId: approval.Id, vc.UpdatedBy: contexts.GetUserId(ctx), vc.Version: gdb.Raw(vc.Version + "+1")}).Update()
if err != nil {
return nil, gerror.Wrap(err, "更新等级审批状态失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return nil, gerror.New("等级规则已更新,请刷新后重试")
}
return &sysin.MemberLevelApprovalModel{ApprovalId: approval.Id, ApprovalNo: approval.ApprovalNo}, nil
}
func (s *sSysMemberLevel) Publish(ctx context.Context, in *sysin.MemberLevelPublishInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
vc, lc := dao.AppMemberLevelVersion.Columns(), dao.AppMemberLevel.Columns()
var v *entity.AppMemberLevelVersion
if err := s.versionModel(ctx).WherePri(in.VersionId).LockUpdate().Scan(&v); err != nil {
return err
}
if v == nil {
return gerror.New("等级规则版本不存在")
}
if v.PublishStatus != consts.BizPublishPending {
return gerror.New("仅待审版本可以发布")
}
if v.Version != in.Version {
return gerror.New("等级规则已更新,请刷新后重试")
}
var approval *entity.ApprovalRequest
if err := service.SysApproval().Model(ctx).WherePri(v.ApprovalRequestId).Scan(&approval); err != nil {
return err
}
if approval == nil || approval.ApprovalStatus != consts.ApprovalStatusApproved {
return gerror.New("审批尚未通过")
}
if v.EffectiveAt != nil && v.EffectiveAt.After(gtime.Now()) {
return gerror.New("计划生效时间尚未到达")
}
_, _ = s.versionModel(ctx).Where(vc.LevelId, v.LevelId).Where(vc.PublishStatus, consts.BizPublishPublished).Data(g.Map{vc.PublishStatus: consts.BizPublishArchived, vc.UpdatedBy: contexts.GetUserId(ctx)}).Update()
result, err := s.versionModel(ctx).WherePri(v.Id).Where(vc.Version, v.Version).Data(g.Map{vc.PublishStatus: consts.BizPublishPublished, vc.PublishedAt: gtime.Now(), vc.UpdatedBy: contexts.GetUserId(ctx), vc.Version: gdb.Raw(vc.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "发布等级规则失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return gerror.New("等级规则已更新,请刷新后重试")
}
if _, err = s.model(ctx).WherePri(v.LevelId).Data(g.Map{lc.CurrentVersionId: v.Id, lc.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return err
}
_, err = service.SysApproval().Model(ctx).WherePri(approval.Id).Data(g.Map{dao.ApprovalRequest.Columns().ExecutedAt: gtime.Now(), dao.ApprovalRequest.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return err
}
_ = dict.ClearFuncCache("memberLevelOption")
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member_level", Action: "level.publish", TargetType: "app_member_level", TargetId: v.LevelId, After: v, ApprovalNo: approval.ApprovalNo, Reason: v.Remark})
})
}
func (s *sSysMemberLevel) Rollback(ctx context.Context, in *sysin.MemberLevelRollbackInp) (versionId uint64, err error) {
var source *entity.AppMemberLevelVersion
if err = s.versionModel(ctx).WherePri(in.VersionId).Scan(&source); err != nil {
return
}
if source == nil {
return 0, gerror.New("历史版本不存在")
}
var level *entity.AppMemberLevel
if err = s.model(ctx).WherePri(source.LevelId).Scan(&level); err != nil {
return
}
if level == nil {
return 0, gerror.New("会员等级不存在")
}
_, versionId, err = s.Save(ctx, &sysin.MemberLevelSaveInp{Id: level.Id, LevelCode: level.LevelCode, Name: level.Name, IconAttachmentId: level.IconAttachmentId, IconUrl: level.IconUrl, UpgradeAmount: source.UpgradeAmount, GiftAmount: source.GiftAmount, DailyOrderLimit: source.DailyOrderLimit, CommissionRate: source.CommissionRate, DailyWithdrawLimit: source.DailyWithdrawLimit, RuleJson: source.RuleJson, EffectiveAt: nil, Sort: level.Sort, Status: level.Status, Remark: "回滚自 V" + fmt.Sprint(source.VersionNo) + ":" + in.Reason})
return
}
func (s *sSysMemberLevel) History(ctx context.Context, in *sysin.MemberLevelHistoryInp) (list []*entity.AppMemberLevelVersion, total int, err error) {
c := dao.AppMemberLevelVersion.Columns()
err = s.versionModel(ctx).Where(c.LevelId, in.LevelId).Page(in.Page, in.PerPage).OrderDesc(c.VersionNo).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取等级版本历史失败")
}
return
}
func (s *sSysMemberLevel) Status(ctx context.Context, in *sysin.MemberLevelStatusInp) error {
c := dao.AppMemberLevel.Columns()
_, err := s.model(ctx).WherePri(in.Id).Data(g.Map{c.Status: in.Status, c.UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return gerror.Wrap(err, "更新等级状态失败")
}
_ = dict.ClearFuncCache("memberLevelOption")
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member_level", Action: "level.status", TargetType: "app_member_level", TargetId: in.Id, After: g.Map{"status": in.Status}})
}
func (s *sSysMemberLevel) Delete(ctx context.Context, in *sysin.MemberLevelDeleteInp) error {
count, err := handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(dao.AppMember.Columns().LevelId, in.Id).Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("该等级仍有会员使用,不能删除,请改为停用")
}
count, err = handler.Model(dao.TaskHallLevel.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(dao.TaskHallLevel.Columns().LevelId, in.Id).Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("该等级仍被任务大厅引用,不能删除")
}
c := dao.AppMemberLevel.Columns()
_, err = s.model(ctx).WherePri(in.Id).Data(g.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx), c.UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return gerror.Wrap(err, "删除会员等级失败")
}
_ = dict.ClearFuncCache("memberLevelOption")
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "member_level", Action: "level.delete", TargetType: "app_member_level", TargetId: in.Id, Reason: in.Reason})
}
func (s *sSysMemberLevel) Option(ctx context.Context) (opts []*model.Option, err error) {
var rows []*entity.AppMemberLevel
c := dao.AppMemberLevel.Columns()
if err = s.model(ctx).Fields(c.Id, c.Name).Where(c.Status, consts.StatusEnabled).WhereGT(c.CurrentVersionId, 0).OrderAsc(c.Sort).OrderAsc(c.Id).Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "获取会员等级选项失败")
}
opts = make([]*model.Option, len(rows))
for i, row := range rows {
opts[i] = dict.GenHashOption(row.Id, gconv.String(row.Name))
}
return
}
@@ -0,0 +1,176 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2024 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
// @AutoGenerate Version 2.15.7
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"hotgo/utility/tree"
)
type sSysNormalTreeDemo struct{}
func NewSysNormalTreeDemo() *sSysNormalTreeDemo {
return &sSysNormalTreeDemo{}
}
func init() {
service.RegisterSysNormalTreeDemo(NewSysNormalTreeDemo())
}
// Model 普通树表ORM模型
func (s *sSysNormalTreeDemo) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(dao.SysGenTreeDemo.Ctx(ctx), option...)
}
// List 获取普通树表列表
func (s *sSysNormalTreeDemo) List(ctx context.Context, in *sysin.NormalTreeDemoListInp) (list []*sysin.NormalTreeDemoListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(sysin.NormalTreeDemoListModel{})
// 查询标题
if in.Title != "" {
mod = mod.WhereLike(dao.SysGenTreeDemo.Columns().Title, "%"+in.Title+"%")
}
// 查询上级
if in.Pid > 0 {
mod = mod.Where(dao.SysGenTreeDemo.Columns().Pid, in.Pid)
}
// 查询测试分类
if in.CategoryId > 0 {
mod = mod.Where(dao.SysGenTreeDemo.Columns().CategoryId, in.CategoryId)
}
// 查询状态
if in.Status > 0 {
mod = mod.Where(dao.SysGenTreeDemo.Columns().Status, in.Status)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.SysGenTreeDemo.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 树形列表判断是否需要分页
if in.Pagination {
mod = mod.Page(in.Page, in.PerPage)
}
// 排序
mod = mod.OrderAsc(dao.SysGenTreeDemo.Columns().Sort).OrderDesc(dao.SysGenTreeDemo.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取普通树表列表失败,请稍后重试!")
return
}
return
}
// Edit 修改/新增普通树表
func (s *sSysNormalTreeDemo) Edit(ctx context.Context, in *sysin.NormalTreeDemoEditInp) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
in.Pid, in.Level, in.Tree, err = hgorm.AutoUpdateTree(ctx, &dao.SysGenTreeDemo, in.Id, in.Pid)
if err != nil {
return err
}
// 修改
if in.Id > 0 {
in.UpdatedBy = contexts.GetUserId(ctx)
if _, err = s.Model(ctx).
Fields(sysin.NormalTreeDemoUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改普通树表失败,请稍后重试!")
}
return
}
// 新增
in.CreatedBy = contexts.GetUserId(ctx)
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(sysin.NormalTreeDemoInsertFields{}).
Data(in).OmitEmptyData().Insert(); err != nil {
err = gerror.Wrap(err, "新增普通树表失败,请稍后重试!")
}
return
})
}
// Delete 删除普通树表
func (s *sSysNormalTreeDemo) Delete(ctx context.Context, in *sysin.NormalTreeDemoDeleteInp) (err error) {
count, err := dao.SysGenTreeDemo.Ctx(ctx).Where(dao.SysGenTreeDemo.Columns().Pid, in.Id).Count()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
if count > 0 {
return gerror.New("请先删除该普通树表下的所有下级!")
}
if _, err = s.Model(ctx).WherePri(in.Id).Delete(); err != nil {
err = gerror.Wrap(err, "删除普通树表失败,请稍后重试!")
return
}
return
}
// MaxSort 获取普通树表最大排序
func (s *sSysNormalTreeDemo) MaxSort(ctx context.Context, in *sysin.NormalTreeDemoMaxSortInp) (res *sysin.NormalTreeDemoMaxSortModel, err error) {
if err = dao.SysGenTreeDemo.Ctx(ctx).Fields(dao.SysGenTreeDemo.Columns().Sort).OrderDesc(dao.SysGenTreeDemo.Columns().Sort).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取普通树表最大排序,请稍后重试!")
return
}
if res == nil {
res = new(sysin.NormalTreeDemoMaxSortModel)
}
res.Sort = form.DefaultMaxSort(res.Sort)
return
}
// View 获取普通树表指定信息
func (s *sSysNormalTreeDemo) View(ctx context.Context, in *sysin.NormalTreeDemoViewInp) (res *sysin.NormalTreeDemoViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取普通树表信息,请稍后重试!")
return
}
return
}
// TreeOption 获取普通树表关系树选项
func (s *sSysNormalTreeDemo) TreeOption(ctx context.Context) (nodes []tree.Node, err error) {
var models []*sysin.NormalTreeDemoTreeOption
if err = s.Model(ctx).Fields(sysin.NormalTreeDemoTreeOption{}).OrderAsc(dao.SysGenTreeDemo.Columns().Pid).OrderAsc(dao.SysGenTreeDemo.Columns().Sort).OrderDesc(dao.SysGenTreeDemo.Columns().Id).Scan(&models); err != nil {
err = gerror.Wrap(err, "获取普通树表关系树选项失败!")
return
}
nodes = make([]tree.Node, len(models))
for i, v := range models {
nodes[i] = v
}
return tree.ListToTree(0, nodes)
}
@@ -0,0 +1,177 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2025 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
// @AutoGenerate Version 2.17.8
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"hotgo/utility/tree"
)
type sSysOptionTreeDemo struct{}
func NewSysOptionTreeDemo() *sSysOptionTreeDemo {
return &sSysOptionTreeDemo{}
}
func init() {
service.RegisterSysOptionTreeDemo(NewSysOptionTreeDemo())
}
// Model 选项树表ORM模型
func (s *sSysOptionTreeDemo) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(dao.SysGenTreeDemo.Ctx(ctx), option...)
}
// List 获取选项树表列表
func (s *sSysOptionTreeDemo) List(ctx context.Context, in *sysin.OptionTreeDemoListInp) (list []*sysin.OptionTreeDemoListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(sysin.OptionTreeDemoListModel{})
// 查询标题
if in.Title != "" {
mod = mod.WhereLike(dao.SysGenTreeDemo.Columns().Title, "%"+in.Title+"%")
}
// 查询上级
if in.Pid > 0 {
mod = mod.Where(dao.SysGenTreeDemo.Columns().Pid, in.Pid)
}
// 查询测试分类
if in.CategoryId > 0 {
mod = mod.Where(dao.SysGenTreeDemo.Columns().CategoryId, in.CategoryId)
}
// 查询状态
if in.Status > 0 {
mod = mod.Where(dao.SysGenTreeDemo.Columns().Status, in.Status)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.SysGenTreeDemo.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 树形列表判断是否需要分页
if in.Pagination {
mod = mod.Page(in.Page, in.PerPage)
}
// 排序
mod = mod.OrderAsc(dao.SysGenTreeDemo.Columns().Sort).OrderDesc(dao.SysGenTreeDemo.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取选项树表列表失败,请稍后重试!")
return
}
return
}
// Edit 修改/新增选项树表
func (s *sSysOptionTreeDemo) Edit(ctx context.Context, in *sysin.OptionTreeDemoEditInp) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
in.Pid, in.Level, in.Tree, err = hgorm.AutoUpdateTree(ctx, &dao.SysGenTreeDemo, in.Id, in.Pid)
if err != nil {
return err
}
// 修改
if in.Id > 0 {
in.UpdatedBy = contexts.GetUserId(ctx)
if _, err = s.Model(ctx).
Fields(sysin.OptionTreeDemoUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改选项树表失败,请稍后重试!")
}
return
}
// 新增
in.CreatedBy = contexts.GetUserId(ctx)
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(sysin.OptionTreeDemoInsertFields{}).
Data(in).OmitEmptyData().Insert(); err != nil {
err = gerror.Wrap(err, "新增选项树表失败,请稍后重试!")
}
return
})
}
// Delete 删除选项树表
func (s *sSysOptionTreeDemo) Delete(ctx context.Context, in *sysin.OptionTreeDemoDeleteInp) (err error) {
count, err := dao.SysGenTreeDemo.Ctx(ctx).Where(dao.SysGenTreeDemo.Columns().Pid, in.Id).Count()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return err
}
if count > 0 {
return gerror.New("请先删除该选项树表下的所有下级!")
}
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除选项树表失败,请稍后重试!")
return
}
return
}
// MaxSort 获取选项树表最大排序
func (s *sSysOptionTreeDemo) MaxSort(ctx context.Context, in *sysin.OptionTreeDemoMaxSortInp) (res *sysin.OptionTreeDemoMaxSortModel, err error) {
if err = dao.SysGenTreeDemo.Ctx(ctx).Fields(dao.SysGenTreeDemo.Columns().Sort).OrderDesc(dao.SysGenTreeDemo.Columns().Sort).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取选项树表最大排序,请稍后重试!")
return
}
if res == nil {
res = new(sysin.OptionTreeDemoMaxSortModel)
}
res.Sort = form.DefaultMaxSort(res.Sort)
return
}
// View 获取选项树表指定信息
func (s *sSysOptionTreeDemo) View(ctx context.Context, in *sysin.OptionTreeDemoViewInp) (res *sysin.OptionTreeDemoViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取选项树表信息,请稍后重试!")
return
}
return
}
// TreeOption 获取选项树表关系树选项
func (s *sSysOptionTreeDemo) TreeOption(ctx context.Context) (nodes []tree.Node, err error) {
var models []*sysin.OptionTreeDemoTreeOption
if err = s.Model(ctx).Fields(sysin.OptionTreeDemoTreeOption{}).OrderAsc(dao.SysGenTreeDemo.Columns().Pid).OrderAsc(dao.SysGenTreeDemo.Columns().Sort).OrderDesc(dao.SysGenTreeDemo.Columns().Id).Scan(&models); err != nil {
err = gerror.Wrap(err, "获取选项树表关系树选项失败!")
return
}
nodes = make([]tree.Node, len(models))
for i, v := range models {
nodes[i] = v
}
return tree.ListToTree(0, nodes)
}
+586
View File
@@ -0,0 +1,586 @@
package sys
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strconv"
"strings"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"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/os/gtime"
"github.com/gogf/gf/v2/util/grand"
)
type sSysPortal struct{}
func NewSysPortal() *sSysPortal { return &sSysPortal{} }
func init() { service.RegisterSysPortal(NewSysPortal()) }
func (s *sSysPortal) siteModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.SiteConfigVersion.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysPortal) contentModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.Content.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysPortal) contentVersionModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.ContentVersion.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysPortal) bannerModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.Banner.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysPortal) productModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.Product.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysPortal) SiteConfigCurrent(ctx context.Context, in *sysin.SiteConfigCurrentInp) (res *sysin.SiteConfigCurrentModel, err error) {
res = new(sysin.SiteConfigCurrentModel)
c := dao.SiteConfigVersion.Columns()
if err = s.siteModel(ctx).Where(c.ConfigGroup, in.ConfigGroup).WhereIn(c.PublishStatus, []int{consts.BizPublishDraft, consts.BizPublishPending}).OrderDesc(c.Id).Limit(1).Scan(&res.Draft); err != nil {
return nil, gerror.Wrap(err, "获取站点配置草稿失败")
}
if err = s.siteModel(ctx).Where(c.ConfigGroup, in.ConfigGroup).Where(c.PublishStatus, consts.BizPublishPublished).OrderDesc(c.Id).Limit(1).Scan(&res.Published); err != nil {
return nil, gerror.Wrap(err, "获取已发布站点配置失败")
}
return res, nil
}
func (s *sSysPortal) SiteConfigHistory(ctx context.Context, in *sysin.SiteConfigHistoryInp) (list []*sysin.SiteConfigHistoryModel, totalCount int, err error) {
c := dao.SiteConfigVersion.Columns()
err = s.siteModel(ctx).Fields(sysin.SiteConfigHistoryModel{}).Where(c.ConfigGroup, in.ConfigGroup).Page(in.Page, in.PerPage).OrderDesc(c.Id).ScanAndCount(&list, &totalCount, false)
if err != nil {
err = gerror.Wrap(err, "获取配置版本记录失败")
}
return
}
func (s *sSysPortal) SiteConfigSave(ctx context.Context, in *sysin.SiteConfigSaveInp) (id uint64, err error) {
raw, err := json.Marshal(in.Config)
if err != nil {
return 0, gerror.Wrap(err, "序列化站点配置失败")
}
sum := sha256.Sum256(raw)
digest := hex.EncodeToString(sum[:])
userId := uint64(contexts.GetUserId(ctx))
c := dao.SiteConfigVersion.Columns()
now := gtime.Now()
if in.Id > 0 {
_, _ = s.siteModel(ctx).
Where(c.ConfigGroup, in.ConfigGroup).
Where(c.PublishStatus, consts.BizPublishPublished).
WhereNot(c.Id, in.Id).
Data(gdb.Map{c.PublishStatus: consts.BizPublishOffline, c.UpdatedBy: userId}).
Update()
result, updateErr := s.siteModel(ctx).
WherePri(in.Id).
Where(c.Version, in.Version).
Data(gdb.Map{
c.ConfigJson: gjson.New(raw),
c.ConfigDigest: digest,
c.PublishStatus: consts.BizPublishPublished,
c.EffectiveAt: in.EffectiveAt,
c.PublishedAt: now,
c.ApprovalRequestId: 0,
c.Remark: in.Remark,
c.UpdatedBy: userId,
c.Version: gdb.Raw(c.Version + "+1"),
}).Update()
if updateErr != nil {
return 0, gerror.Wrap(updateErr, "保存站点配置失败")
}
rows, _ := result.RowsAffected()
if rows == 0 {
return 0, gerror.New("配置已更新,请刷新后重试")
}
return in.Id, nil
}
_, _ = s.siteModel(ctx).
Where(c.ConfigGroup, in.ConfigGroup).
Where(c.PublishStatus, consts.BizPublishPublished).
Data(gdb.Map{c.PublishStatus: consts.BizPublishOffline, c.UpdatedBy: userId}).
Update()
configNo := "CFG" + gtime.Now().Format("YmdHis") + grand.S(5)
result, err := s.siteModel(ctx).
Fields(c.ConfigNo, c.ConfigGroup, c.SchemaVersion, c.ConfigJson, c.ConfigDigest, c.PublishStatus, c.EffectiveAt, c.PublishedAt, c.BaseVersionId, c.Version, c.Remark, c.CreatedBy, c.UpdatedBy).
Hook(hook.SaveTenant).
Data(gdb.Map{
c.ConfigNo: configNo,
c.ConfigGroup: in.ConfigGroup,
c.SchemaVersion: "1.0",
c.ConfigJson: gjson.New(raw),
c.ConfigDigest: digest,
c.PublishStatus: consts.BizPublishPublished,
c.EffectiveAt: in.EffectiveAt,
c.PublishedAt: now,
c.CreatedBy: userId,
c.UpdatedBy: userId,
}).Insert()
if err != nil {
return 0, gerror.Wrap(err, "保存站点配置失败")
}
last, _ := result.LastInsertId()
return uint64(last), nil
}
func (s *sSysPortal) SiteConfigSubmit(ctx context.Context, in *sysin.SiteConfigSubmitInp) (res *sysin.PortalApprovalModel, err error) {
var row *entity.SiteConfigVersion
c := dao.SiteConfigVersion.Columns()
if err = s.siteModel(ctx).WherePri(in.Id).Scan(&row); err != nil {
return nil, gerror.Wrap(err, "读取配置草稿失败")
}
if row == nil || row.Id == 0 {
return nil, gerror.New("配置草稿不存在")
}
if row.PublishStatus != consts.BizPublishDraft {
return nil, gerror.New("仅草稿可以提交审批")
}
if row.Version != in.Version {
return nil, gerror.New("配置已更新,请刷新后重试")
}
payload := gjson.New(row)
approval, err := service.SysApproval().Create(ctx, &sysin.ApprovalCreateInp{ApprovalType: "site_config_publish", TargetType: "site_config_version", TargetId: row.Id, Action: "publish", Title: "发布站点配置 " + row.ConfigNo, PayloadJson: payload, RiskLevel: consts.ApprovalRiskHigh, RequiredApprovals: 1, Remark: in.Reason})
if err != nil {
return nil, err
}
if err = service.SysApproval().Submit(ctx, &sysin.ApprovalSubmitInp{Id: approval.Id, Version: 0}); err != nil {
return nil, err
}
result, err := s.siteModel(ctx).WherePri(row.Id).Where(c.Version, row.Version).Data(gdb.Map{c.PublishStatus: consts.BizPublishPending, c.ApprovalRequestId: approval.Id, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return nil, gerror.Wrap(err, "更新配置审批状态失败")
}
rows, _ := result.RowsAffected()
if rows == 0 {
return nil, gerror.New("配置已更新,请刷新后重试")
}
return &sysin.PortalApprovalModel{ApprovalId: approval.Id, ApprovalNo: approval.ApprovalNo}, nil
}
func (s *sSysPortal) SiteConfigPublish(ctx context.Context, in *sysin.SiteConfigPublishInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var row *entity.SiteConfigVersion
c := dao.SiteConfigVersion.Columns()
if err := s.siteModel(ctx).WherePri(in.Id).LockUpdate().Scan(&row); err != nil {
return gerror.Wrap(err, "读取配置版本失败")
}
if row == nil {
return gerror.New("配置版本不存在")
}
if row.Version != in.Version {
return gerror.New("配置已更新,请刷新后重试")
}
if row.PublishStatus != consts.BizPublishPending {
return gerror.New("仅待审配置可以发布")
}
var approval *entity.ApprovalRequest
if err := service.SysApproval().Model(ctx).WherePri(row.ApprovalRequestId).Scan(&approval); err != nil {
return err
}
if approval == nil || approval.ApprovalStatus != consts.ApprovalStatusApproved {
return gerror.New("审批尚未通过,不能发布")
}
now := gtime.Now()
if row.EffectiveAt != nil && row.EffectiveAt.After(now) {
return gerror.New("计划生效时间尚未到达")
}
_, _ = s.siteModel(ctx).Where(c.ConfigGroup, row.ConfigGroup).Where(c.PublishStatus, consts.BizPublishPublished).Data(gdb.Map{c.PublishStatus: consts.BizPublishOffline, c.UpdatedBy: contexts.GetUserId(ctx)}).Update()
_, err := s.siteModel(ctx).WherePri(row.Id).Where(c.Version, row.Version).Data(gdb.Map{c.PublishStatus: consts.BizPublishPublished, c.PublishedAt: now, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "发布站点配置失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "portal", Action: "publish", TargetType: "site_config_version", TargetId: row.Id, After: row, ApprovalNo: approval.ApprovalNo, Reason: "审批通过后发布"})
})
}
func (s *sSysPortal) SiteConfigRollback(ctx context.Context, in *sysin.SiteConfigRollbackInp) (id uint64, err error) {
var source *entity.SiteConfigVersion
if err = s.siteModel(ctx).WherePri(in.Id).Scan(&source); err != nil {
return 0, gerror.Wrap(err, "读取历史配置失败")
}
if source == nil {
return 0, gerror.New("历史配置不存在")
}
save := &sysin.SiteConfigSaveInp{ConfigGroup: source.ConfigGroup, Remark: "回滚自 " + source.ConfigNo + ":" + in.Reason}
if err = source.ConfigJson.Scan(&save.Config); err != nil {
return 0, gerror.Wrap(err, "解析历史配置失败")
}
return s.SiteConfigSave(ctx, save)
}
func (s *sSysPortal) ContentList(ctx context.Context, in *sysin.ContentListInp) (list []*sysin.ContentListModel, totalCount int, err error) {
c := dao.Content.Columns()
mod := s.contentModel(ctx).Fields(sysin.ContentListModel{})
if in.Title != "" {
mod = mod.WhereLike(c.Title, "%"+in.Title+"%")
}
if in.ContentType != "" {
mod = mod.Where(c.ContentType, in.ContentType)
}
if err = mod.Page(in.Page, in.PerPage).OrderDesc(c.Id).ScanAndCount(&list, &totalCount, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取文本内容列表失败")
}
vc := dao.ContentVersion.Columns()
for _, item := range list {
var v *entity.ContentVersion
_ = s.contentVersionModel(ctx).Where(vc.ContentId, item.Id).OrderDesc(vc.VersionNo).Limit(1).Scan(&v)
if v != nil {
item.LatestVersionId = v.Id
item.LatestVersionNo = v.VersionNo
item.Summary = v.Summary
item.CoverUrl = v.CoverUrl
}
}
return
}
func (s *sSysPortal) ContentView(ctx context.Context, in *sysin.ContentViewInp) (res *sysin.ContentViewModel, err error) {
res = new(sysin.ContentViewModel)
if err = s.contentModel(ctx).WherePri(in.Id).Scan(&res.Content); err != nil {
return nil, gerror.Wrap(err, "获取文本内容失败")
}
if res.Id == 0 {
return nil, gerror.New("文本内容不存在")
}
vc := dao.ContentVersion.Columns()
if err = s.contentVersionModel(ctx).Where(vc.ContentId, in.Id).OrderDesc(vc.VersionNo).Scan(&res.Versions); err != nil {
return nil, gerror.Wrap(err, "获取内容版本失败")
}
if len(res.Versions) > 0 {
res.LatestVersion = res.Versions[0]
}
return res, nil
}
func (s *sSysPortal) ContentEdit(ctx context.Context, in *sysin.ContentEditInp) (id uint64, err error) {
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
userId := uint64(contexts.GetUserId(ctx))
c := dao.Content.Columns()
if in.Id == 0 {
result, e := s.contentModel(ctx).Fields(c.ContentKey, c.ContentType, c.Title, c.PublishStatus, c.Sort, c.Version, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(gdb.Map{c.ContentKey: in.ContentKey, c.ContentType: in.ContentType, c.Title: in.Title, c.PublishStatus: consts.BizPublishDraft, c.Sort: in.Sort, c.Remark: in.Remark, c.CreatedBy: userId, c.UpdatedBy: userId}).Insert()
if e != nil {
return gerror.Wrap(e, "创建文本内容失败")
}
last, _ := result.LastInsertId()
id = uint64(last)
} else {
id = in.Id
result, e := s.contentModel(ctx).WherePri(in.Id).Where(c.Version, in.Version).Data(gdb.Map{c.ContentType: in.ContentType, c.Title: in.Title, c.Sort: in.Sort, c.Remark: in.Remark, c.UpdatedBy: userId, c.Version: gdb.Raw(c.Version + "+1")}).Update()
if e != nil {
return gerror.Wrap(e, "更新文本内容失败")
}
rows, _ := result.RowsAffected()
if rows == 0 {
return gerror.New("内容已更新,请刷新后重试")
}
}
vc := dao.ContentVersion.Columns()
maxNo, e := s.contentVersionModel(ctx).Where(vc.ContentId, id).Max(vc.VersionNo)
if e != nil {
return gerror.Wrap(e, "读取内容版本号失败")
}
_, e = s.contentVersionModel(ctx).Fields(vc.ContentId, vc.VersionNo, vc.Title, vc.Summary, vc.BodyHtml, vc.BodyText, vc.CoverUrl, vc.PublishStatus, vc.EffectiveAt, vc.CreatedBy).Hook(hook.SaveTenant).Data(gdb.Map{vc.ContentId: id, vc.VersionNo: uint(maxNo) + 1, vc.Title: in.Title, vc.Summary: in.Summary, vc.BodyHtml: in.BodyHtml, vc.BodyText: in.BodyText, vc.CoverUrl: in.CoverUrl, vc.PublishStatus: consts.BizPublishDraft, vc.EffectiveAt: in.EffectiveAt, vc.CreatedBy: userId}).Insert()
if e != nil {
return gerror.Wrap(e, "创建内容版本失败")
}
return nil
})
return
}
func (s *sSysPortal) ContentDelete(ctx context.Context, in *sysin.ContentDeleteInp) error {
var row *entity.Content
if err := s.contentModel(ctx).WherePri(in.Id).Scan(&row); err != nil {
return err
}
if row == nil {
return gerror.New("文本内容不存在")
}
c := dao.Content.Columns()
if row.PublishStatus == consts.BizPublishPublished {
_, err := s.contentModel(ctx).WherePri(in.Id).Data(gdb.Map{c.PublishStatus: consts.BizPublishOffline, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "下线文本内容失败")
}
return nil
}
_, err := s.contentModel(ctx).WherePri(in.Id).Data(gdb.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx)}).Unscoped().Update()
if err != nil {
return gerror.Wrap(err, "删除文本内容失败")
}
return nil
}
func (s *sSysPortal) ContentPublish(ctx context.Context, in *sysin.ContentPublishInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var content *entity.Content
var version *entity.ContentVersion
if err := s.contentModel(ctx).WherePri(in.Id).LockUpdate().Scan(&content); err != nil {
return err
}
if content == nil {
return gerror.New("文本内容不存在")
}
vc := dao.ContentVersion.Columns()
if err := s.contentVersionModel(ctx).WherePri(in.VersionId).Where(vc.ContentId, in.Id).Scan(&version); err != nil {
return err
}
if version == nil {
return gerror.New("内容版本不存在")
}
if version.EffectiveAt != nil && version.EffectiveAt.After(gtime.Now()) {
return gerror.New("计划发布时间尚未到达")
}
_, _ = s.contentVersionModel(ctx).Where(vc.ContentId, in.Id).Where(vc.PublishStatus, consts.BizPublishPublished).Data(gdb.Map{vc.PublishStatus: consts.BizPublishOffline}).Update()
_, err := s.contentVersionModel(ctx).WherePri(version.Id).Data(gdb.Map{vc.PublishStatus: consts.BizPublishPublished}).Update()
if err != nil {
return gerror.Wrap(err, "发布内容版本失败")
}
c := dao.Content.Columns()
_, err = s.contentModel(ctx).WherePri(in.Id).Data(gdb.Map{c.PublishedVersionId: version.Id, c.PublishStatus: consts.BizPublishPublished, c.PublishedAt: gtime.Now(), c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
return gerror.Wrap(err, "发布文本内容失败")
})
}
func (s *sSysPortal) ContentOffline(ctx context.Context, in *sysin.ContentOfflineInp) error {
c := dao.Content.Columns()
_, err := s.contentModel(ctx).WherePri(in.Id).Data(gdb.Map{c.PublishStatus: consts.BizPublishOffline, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "下线文本内容失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "portal", Action: "offline", TargetType: "content", TargetId: in.Id, Reason: in.Reason})
}
func (s *sSysPortal) BannerList(ctx context.Context, in *sysin.BannerListInp) (list []*sysin.BannerListModel, totalCount int, err error) {
c := dao.Banner.Columns()
mod := s.bannerModel(ctx).Fields(sysin.BannerListModel{})
if in.Name != "" {
mod = mod.WhereLike(c.Name, "%"+in.Name+"%")
}
if in.Position != "" {
mod = mod.Where(c.Position, in.Position)
}
if in.PublishStatus > 0 {
mod = mod.Where(c.PublishStatus, in.PublishStatus)
}
err = mod.Page(in.Page, in.PerPage).OrderAsc(c.Sort).OrderDesc(c.Id).ScanAndCount(&list, &totalCount, false)
if err != nil {
err = gerror.Wrap(err, "获取Banner列表失败")
}
return
}
func (s *sSysPortal) BannerView(ctx context.Context, in *sysin.BannerViewInp) (res *entity.Banner, err error) {
err = s.bannerModel(ctx).WherePri(in.Id).Scan(&res)
if err != nil {
return nil, gerror.Wrap(err, "获取Banner失败")
}
if res == nil {
return nil, gerror.New("Banner不存在")
}
return
}
func (s *sSysPortal) BannerEdit(ctx context.Context, in *sysin.BannerEditInp) (id uint64, err error) {
c := dao.Banner.Columns()
userId := uint64(contexts.GetUserId(ctx))
if in.Id > 0 {
result, e := s.bannerModel(ctx).WherePri(in.Id).Where(c.Version, in.Version).Data(gdb.Map{
c.Name: in.Name, c.Position: in.Position, c.ImageAttachmentId: in.ImageAttachmentId,
c.ImageUrl: in.ImageUrl, c.AltText: in.AltText, c.TargetType: in.TargetType,
c.TargetValue: in.TargetValue, c.StartAt: in.StartAt, c.EndAt: in.EndAt,
c.Sort: in.Sort, c.Remark: in.Remark, c.UpdatedBy: userId, c.Version: gdb.Raw(c.Version + "+1"),
}).Update()
if e != nil {
return 0, gerror.Wrap(e, "更新Banner失败")
}
rows, _ := result.RowsAffected()
if rows == 0 {
return 0, gerror.New("Banner已更新,请刷新后重试")
}
return in.Id, nil
}
in.PublishStatus = consts.BizPublishDraft
in.CreatedBy = userId
in.UpdatedBy = userId
result, e := s.bannerModel(ctx).Fields(c.Name, c.Position, c.ImageAttachmentId, c.ImageUrl, c.AltText, c.TargetType, c.TargetValue, c.StartAt, c.EndAt, c.PublishStatus, c.Sort, c.Version, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(in).Insert()
if e != nil {
return 0, gerror.Wrap(e, "创建Banner失败")
}
last, _ := result.LastInsertId()
return uint64(last), nil
}
func (s *sSysPortal) BannerDelete(ctx context.Context, in *sysin.BannerDeleteInp) error {
var row *entity.Banner
if err := s.bannerModel(ctx).WherePri(in.Id).Scan(&row); err != nil {
return err
}
if row == nil {
return gerror.New("Banner不存在")
}
c := dao.Banner.Columns()
if row.PublishStatus == consts.BizPublishPublished {
_, err := s.bannerModel(ctx).WherePri(in.Id).Data(gdb.Map{c.PublishStatus: consts.BizPublishOffline, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
return err
}
_, err := s.bannerModel(ctx).WherePri(in.Id).Data(gdb.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx)}).Unscoped().Update()
return gerror.Wrap(err, "删除Banner失败")
}
func (s *sSysPortal) BannerStatus(ctx context.Context, in *sysin.BannerStatusInp) error {
c := dao.Banner.Columns()
result, err := s.bannerModel(ctx).WherePri(in.Id).Where(c.Version, in.Version).Data(gdb.Map{c.PublishStatus: in.PublishStatus, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "更新Banner状态失败")
}
rows, _ := result.RowsAffected()
if rows == 0 {
return gerror.New("Banner已更新,请刷新后重试")
}
return nil
}
func (s *sSysPortal) ManualProductList(ctx context.Context, in *sysin.ManualProductListInp) (list []*sysin.ManualProductListModel, totalCount int, err error) {
c := dao.Product.Columns()
mod := s.productModel(ctx).Fields(sysin.ManualProductListModel{}).Where(c.ProductType, consts.ProductTypeManual)
if in.Name != "" {
mod = mod.WhereLike(c.Name, "%"+in.Name+"%")
}
if in.PublishStatus > 0 {
mod = mod.Where(c.PublishStatus, in.PublishStatus)
}
if err = mod.Page(in.Page, in.PerPage).OrderAsc(c.Sort).OrderDesc(c.Id).ScanAndCount(&list, &totalCount, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取手工商城列表失败")
}
for _, item := range list {
item.Price = formatCents(item.PriceAmount)
}
return
}
func (s *sSysPortal) ManualProductView(ctx context.Context, in *sysin.ManualProductViewInp) (res *sysin.ManualProductViewModel, err error) {
c := dao.Product.Columns()
if err = s.productModel(ctx).WherePri(in.Id).Where(c.ProductType, consts.ProductTypeManual).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取手工商城商品失败")
}
if res == nil {
return nil, gerror.New("商品不存在")
}
res.Price = formatCents(res.PriceAmount)
return
}
func (s *sSysPortal) ManualProductEdit(ctx context.Context, in *sysin.ManualProductEditInp) (id uint64, err error) {
amount, err := parseCents(in.Price)
if err != nil {
return 0, err
}
in.PriceAmount = amount
in.ProductType = consts.ProductTypeManual
if in.PaymentType == "" {
in.PaymentType = "balance"
}
if in.Currency == "" {
in.Currency = "CNY"
}
c := dao.Product.Columns()
userId := uint64(contexts.GetUserId(ctx))
if in.Id > 0 {
result, e := s.productModel(ctx).WherePri(in.Id).Where(c.Version, in.Version).Data(gdb.Map{
c.Name: in.Name,
c.PriceAmount: in.PriceAmount,
c.CoverAttachmentId: in.CoverAttachmentId,
c.CoverUrl: in.CoverUrl,
c.PublishStatus: consts.BizPublishPublished,
c.UpdatedBy: userId,
c.Version: gdb.Raw(c.Version + "+1"),
}).Update()
if e != nil {
return 0, gerror.Wrap(e, "更新手工商城商品失败")
}
rows, _ := result.RowsAffected()
if rows == 0 {
return 0, gerror.New("商品已更新,请刷新后重试")
}
return in.Id, nil
}
in.ProductNo = "MP" + gtime.Now().Format("YmdHis") + grand.S(5)
in.PublishStatus = consts.BizPublishPublished
in.CreatedBy = userId
in.UpdatedBy = userId
result, e := s.productModel(ctx).Fields(c.ProductNo, c.CategoryId, c.ProductType, c.PaymentType, c.Name, c.ShopName, c.PriceAmount, c.Currency, c.CoverAttachmentId, c.CoverUrl, c.DetailHtml, c.IsRecommended, c.PublishStatus, c.StartAt, c.EndAt, c.Sort, c.Version, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(in).Insert()
if e != nil {
return 0, gerror.Wrap(e, "创建手工商城商品失败")
}
last, _ := result.LastInsertId()
return uint64(last), nil
}
func (s *sSysPortal) ManualProductDelete(ctx context.Context, in *sysin.ManualProductDeleteInp) error {
c := dao.Product.Columns()
_, err := s.productModel(ctx).
WhereIn(c.Id, in.Ids).
Where(c.ProductType, consts.ProductTypeManual).
Data(gdb.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx)}).
Unscoped().
Update()
return gerror.Wrap(err, "删除商品失败")
}
func (s *sSysPortal) ManualProductStatus(ctx context.Context, in *sysin.ManualProductStatusInp) error {
c := dao.Product.Columns()
result, err := s.productModel(ctx).WherePri(in.Id).Where(c.Version, in.Version).Where(c.ProductType, consts.ProductTypeManual).Data(gdb.Map{c.PublishStatus: in.PublishStatus, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "更新商品状态失败")
}
rows, _ := result.RowsAffected()
if rows == 0 {
return gerror.New("商品已更新,请刷新后重试")
}
return nil
}
func parseCents(value string) (int64, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, gerror.New("价格不能为空")
}
if strings.HasPrefix(value, "-") {
return 0, gerror.New("价格不能为负数")
}
parts := strings.Split(value, ".")
if len(parts) > 2 {
return 0, gerror.New("价格格式不正确")
}
yuan, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return 0, gerror.New("价格格式不正确")
}
fraction := ""
if len(parts) == 2 {
fraction = parts[1]
}
if len(fraction) > 2 {
return 0, gerror.New("价格最多保留两位小数")
}
for len(fraction) < 2 {
fraction += "0"
}
fen := int64(0)
if fraction != "" {
fen, err = strconv.ParseInt(fraction, 10, 64)
if err != nil {
return 0, gerror.New("价格格式不正确")
}
}
if yuan > (1<<63-1-fen)/100 {
return 0, gerror.New("价格超出允许范围")
}
return yuan*100 + fen, nil
}
func formatCents(value int64) string { return fmt.Sprintf("%d.%02d", value/100, value%100) }
+32
View File
@@ -0,0 +1,32 @@
package sys
import "testing"
func TestParseCents(t *testing.T) {
tests := []struct {
value string
want int64
}{
{"0", 0},
{"0.01", 1},
{"12.3", 1230},
{"999.99", 99999},
}
for _, item := range tests {
got, err := parseCents(item.value)
if err != nil {
t.Fatalf("parseCents(%q) returned error: %v", item.value, err)
}
if got != item.want {
t.Fatalf("parseCents(%q)=%d, want %d", item.value, got, item.want)
}
}
}
func TestParseCentsRejectsInvalidValues(t *testing.T) {
for _, value := range []string{"", "-1", "1.001", "abc", "1.2.3"} {
if _, err := parseCents(value); err == nil {
t.Fatalf("parseCents(%q) should fail", value)
}
}
}
+194
View File
@@ -0,0 +1,194 @@
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/dict"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"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/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/grand"
)
type sSysProduct struct{}
func NewSysProduct() *sSysProduct { return &sSysProduct{} }
func init() {
service.RegisterSysProduct(NewSysProduct())
dict.RegisterFunc("productCategoryOption", "商品分类选项", service.SysProduct().CategoryOption, true)
}
func (s *sSysProduct) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{FilterTenant: true})
}
return handler.Model(dao.Product.Ctx(ctx), option...)
}
func (s *sSysProduct) categoryModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.ProductCategory.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysProduct) listModel(ctx context.Context) *gdb.Model {
pc, cc := dao.Product.Columns(), dao.ProductCategory.Columns()
return s.Model(ctx).As("p").LeftJoin(dao.ProductCategory.Table()+" c", col("c", cc.Id)+"="+col("p", pc.CategoryId)).Fields("p.*", fmt.Sprintf("COALESCE(%s,'') category_name", col("c", cc.Name)))
}
func (s *sSysProduct) List(ctx context.Context, in *sysin.ProductListInp) (list []*sysin.ProductListModel, total int, err error) {
pc := dao.Product.Columns()
mod := s.listModel(ctx)
if in.Name != "" {
mod = mod.WhereLike(col("p", pc.Name), "%"+in.Name+"%")
}
if in.PaymentType != "" {
mod = mod.Where(col("p", pc.PaymentType), in.PaymentType)
}
if in.CategoryId > 0 {
mod = mod.Where(col("p", pc.CategoryId), in.CategoryId)
}
if in.PublishStatus > 0 {
mod = mod.Where(col("p", pc.PublishStatus), in.PublishStatus)
}
if err = mod.Page(in.Page, in.PerPage).OrderAsc(col("p", pc.Sort)).OrderDesc(col("p", pc.Id)).ScanAndCount(&list, &total, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取商品列表失败")
}
for _, row := range list {
row.Price = formatCents(row.PriceAmount)
count, e := handler.Model(dao.TradeOrder.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(dao.TradeOrder.Columns().ProductId, row.Id).Count()
if e != nil {
return nil, 0, e
}
row.Referenced = count > 0
}
return
}
func (s *sSysProduct) View(ctx context.Context, in *sysin.ProductViewInp) (res *sysin.ProductListModel, err error) {
if err = s.listModel(ctx).Where(col("p", dao.Product.Columns().Id), in.Id).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取商品详情失败")
}
if res == nil {
return nil, gerror.New("商品不存在")
}
res.Price = formatCents(res.PriceAmount)
count, err := handler.Model(dao.TradeOrder.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(dao.TradeOrder.Columns().ProductId, res.Id).Count()
res.Referenced = count > 0
return
}
func (s *sSysProduct) Edit(ctx context.Context, in *sysin.ProductEditInp) (id uint64, err error) {
amount, err := parseCents(in.Price)
if err != nil {
return 0, err
}
in.PriceAmount = amount
if in.PaymentType == "" {
in.PaymentType = consts.PayTypeBalance
}
if in.Currency == "" {
in.Currency = "CNY"
}
cc := dao.ProductCategory.Columns()
count, err := s.categoryModel(ctx).WherePri(in.CategoryId).Where(cc.Status, consts.StatusEnabled).Count()
if err != nil {
return 0, err
}
if count == 0 {
return 0, gerror.New("商品分类不存在或已停用")
}
c := dao.Product.Columns()
uid := contexts.GetUserId(ctx)
var before *entity.Product
if in.Id > 0 {
if err = s.Model(ctx).WherePri(in.Id).Scan(&before); err != nil {
return
}
if before == nil {
return 0, gerror.New("商品不存在")
}
result, e := s.Model(ctx).WherePri(in.Id).Where(c.Version, in.Version).Fields(sysin.ProductUpdateFields{}).Data(g.Map{c.CategoryId: in.CategoryId, c.ProductType: in.ProductType, c.PaymentType: in.PaymentType, c.Name: in.Name, c.ShopName: in.ShopName, c.PriceAmount: in.PriceAmount, c.Currency: in.Currency, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.DetailHtml: in.DetailHtml, c.IsRecommended: in.IsRecommended, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.Sort: in.Sort, c.Remark: in.Remark, c.UpdatedBy: uid, c.Version: gdb.Raw(c.Version + "+1")}).Update()
if e != nil {
return 0, gerror.Wrap(e, "更新商品失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return 0, gerror.New("商品数据已更新,请刷新后重试")
}
id = in.Id
} else {
in.ProductNo = "PD" + gtime.Now().Format("YmdHis") + grand.S(5)
result, e := s.Model(ctx).Fields(c.ProductNo, c.CategoryId, c.ProductType, c.PaymentType, c.Name, c.ShopName, c.PriceAmount, c.Currency, c.CoverAttachmentId, c.CoverUrl, c.DetailHtml, c.IsRecommended, c.PublishStatus, c.StartAt, c.EndAt, c.Sort, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.ProductNo: in.ProductNo, c.CategoryId: in.CategoryId, c.ProductType: in.ProductType, c.PaymentType: in.PaymentType, c.Name: in.Name, c.ShopName: in.ShopName, c.PriceAmount: in.PriceAmount, c.Currency: in.Currency, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.DetailHtml: in.DetailHtml, c.IsRecommended: in.IsRecommended, c.PublishStatus: consts.BizPublishDraft, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.Sort: in.Sort, c.Remark: in.Remark, c.CreatedBy: uid, c.UpdatedBy: uid}).Insert()
if e != nil {
return 0, gerror.Wrap(e, "创建商品失败")
}
last, _ := result.LastInsertId()
id = uint64(last)
}
err = service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "product", Action: "product.edit", TargetType: "product", TargetId: id, Before: before, After: g.Map{"name": in.Name, "priceAmount": in.PriceAmount, "categoryId": in.CategoryId}})
return
}
func (s *sSysProduct) Status(ctx context.Context, in *sysin.ProductStatusInp) error {
c := dao.Product.Columns()
result, err := s.Model(ctx).WherePri(in.Id).Where(c.Version, in.Version).Data(g.Map{c.PublishStatus: in.PublishStatus, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "更新商品状态失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return gerror.New("商品数据已更新,请刷新后重试")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "product", Action: "product.status", TargetType: "product", TargetId: in.Id, After: g.Map{"publishStatus": in.PublishStatus}})
}
func (s *sSysProduct) Recommend(ctx context.Context, in *sysin.ProductRecommendInp) error {
c := dao.Product.Columns()
result, err := s.Model(ctx).WherePri(in.Id).Where(c.Version, in.Version).Data(g.Map{c.IsRecommended: in.IsRecommended, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "更新商品推荐状态失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return gerror.New("商品数据已更新,请刷新后重试")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "product", Action: "product.recommend", TargetType: "product", TargetId: in.Id, After: g.Map{"isRecommended": in.IsRecommended}})
}
func (s *sSysProduct) Delete(ctx context.Context, in *sysin.ProductDeleteInp) error {
c := dao.Product.Columns()
var row *entity.Product
if err := s.Model(ctx).WherePri(in.Id).Scan(&row); err != nil {
return err
}
if row == nil {
return gerror.New("商品不存在")
}
count, err := handler.Model(dao.TradeOrder.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(dao.TradeOrder.Columns().ProductId, in.Id).Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("商品已被订单引用,只允许下架,不能删除")
}
if row.PublishStatus == consts.BizPublishPublished {
_, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{c.PublishStatus: consts.BizPublishOffline, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
return err
}
_, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx), c.UpdatedBy: contexts.GetUserId(ctx)}).Update()
return gerror.Wrap(err, "删除商品失败")
}
func (s *sSysProduct) CategoryOption(ctx context.Context) (opts []*model.Option, err error) {
var rows []*entity.ProductCategory
c := dao.ProductCategory.Columns()
if err = s.categoryModel(ctx).Fields(c.Id, c.Name).Where(c.Status, consts.StatusEnabled).OrderAsc(c.Sort).Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "获取商品分类选项失败")
}
for _, row := range rows {
opts = append(opts, dict.GenHashOption(row.Id, gconv.String(row.Name)))
}
return
}
+257
View File
@@ -0,0 +1,257 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"fmt"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
type sSysProvinces struct{}
func NewSysProvinces() *sSysProvinces {
return &sSysProvinces{}
}
func init() {
service.RegisterSysProvinces(NewSysProvinces())
}
// Tree 关系树选项列表
func (s *sSysProvinces) Tree(ctx context.Context) (list []*sysin.ProvincesTree, err error) {
var models []*entity.SysProvinces
if err = dao.SysProvinces.Ctx(ctx).Order("pid asc,id asc,sort asc").Scan(&models); err != nil {
err = gerror.Wrap(err, "获取省市区关系树选项列表失败!")
return
}
list = s.treeList(0, models)
return
}
// Delete 删除省市区数据
func (s *sSysProvinces) Delete(ctx context.Context, in *sysin.ProvincesDeleteInp) (err error) {
var models *entity.SysProvinces
if err = dao.SysProvinces.Ctx(ctx).Where("id", in.Id).Scan(&models); err != nil {
err = gerror.Wrap(err, "获取省市区数据失败!")
return
}
if models == nil {
err = gerror.New("数据不存在或已删除!")
return
}
has, err := dao.SysProvinces.Ctx(ctx).Where("pid", models.Id).One()
if err != nil {
err = gerror.Wrap(err, "删除省市区数据时获取上级数据失败!")
return
}
if !has.IsEmpty() {
err = gerror.New("请先删除该地区下得所有子级!")
return
}
if _, err = dao.SysProvinces.Ctx(ctx).Where("id", in.Id).Delete(); err != nil {
err = gerror.Wrap(err, "删除省市区数据失败!")
return
}
return
}
// Edit 修改/新增省市区数据
func (s *sSysProvinces) Edit(ctx context.Context, in *sysin.ProvincesEditInp) (err error) {
// 关系树
in.Pid, in.Level, in.Tree, err = hgorm.GenSubTree(ctx, &dao.SysProvinces, in.Pid)
if err != nil {
return
}
models, err := s.View(ctx, &sysin.ProvincesViewInp{Id: in.Id})
if err != nil {
return
}
// 修改
if models != nil {
if _, err = dao.SysProvinces.Ctx(ctx).Fields(sysin.ProvincesUpdateFields{}).WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改省市区数据失败!")
}
return
}
// 新增
if _, err = dao.SysProvinces.Ctx(ctx).Fields(sysin.ProvincesInsertFields{}).Data(in).OmitEmptyData().Insert(); err != nil {
err = gerror.Wrap(err, "新增省市区数据失败!")
}
return
}
// Status 更新省市区状态
func (s *sSysProvinces) Status(ctx context.Context, in *sysin.ProvincesStatusInp) (err error) {
if _, err = dao.SysProvinces.Ctx(ctx).Where("id", in.Id).Data("status", in.Status).Update(); err != nil {
err = gerror.Wrap(err, "更新省市区状态失败!")
}
return
}
// MaxSort 最大排序
func (s *sSysProvinces) MaxSort(ctx context.Context, in *sysin.ProvincesMaxSortInp) (res *sysin.ProvincesMaxSortModel, err error) {
if err = dao.SysProvinces.Ctx(ctx).Fields(dao.SysProvinces.Columns().Sort).OrderDesc(dao.SysProvinces.Columns().Sort).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取省市区最大排序失败!")
return
}
if res == nil {
res = new(sysin.ProvincesMaxSortModel)
}
res.Sort = form.DefaultMaxSort(res.Sort)
return
}
// View 获取省市区信息
func (s *sSysProvinces) View(ctx context.Context, in *sysin.ProvincesViewInp) (res *sysin.ProvincesViewModel, err error) {
if err = dao.SysProvinces.Ctx(ctx).Where("id", in.Id).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取省市区信息失败!")
}
return
}
// List 获取列表
func (s *sSysProvinces) List(ctx context.Context, in *sysin.ProvincesListInp) (list []*sysin.ProvincesListModel, totalCount int, err error) {
mod := dao.SysProvinces.Ctx(ctx)
if in.Title != "" {
mod = mod.WhereLike("title", "%"+in.Title+"%")
}
if in.Status > 0 {
mod = mod.Where("status", in.Status)
}
totalCount, err = mod.Count()
if err != nil {
err = gerror.Wrap(err, "获取省市区数据行失败!")
return
}
if totalCount == 0 {
return
}
if err = mod.Page(in.Page, in.PerPage).Order("id desc").Scan(&list); err != nil {
err = gerror.Wrap(err, "获取省市区列表失败!")
}
return
}
// ChildrenList 获取省市区下级列表
func (s *sSysProvinces) ChildrenList(ctx context.Context, in *sysin.ProvincesChildrenListInp) (list []*sysin.ProvincesChildrenListModel, totalCount int, err error) {
mod := dao.SysProvinces.Ctx(ctx)
if in.Title != "" {
mod = mod.WhereLike("title", "%"+in.Title+"%")
}
if in.Pid > 0 {
mod = mod.Where("pid", in.Pid)
}
if in.Id > 0 {
mod = mod.Where("id", in.Id)
}
totalCount, err = mod.Count()
if err != nil {
err = gerror.Wrap(err, "获取省市区下级数据行失败!")
return
}
if totalCount == 0 {
return
}
if err = mod.Page(in.Page, in.PerPage).Order("sort asc,id desc").Scan(&list); err != nil {
err = gerror.Wrap(err, "获取省市区下级列表失败!")
}
return
}
// UniqueId 获取省市区下级列表
func (s *sSysProvinces) UniqueId(ctx context.Context, in *sysin.ProvincesUniqueIdInp) (res *sysin.ProvincesUniqueIdModel, err error) {
res = new(sysin.ProvincesUniqueIdModel)
res.IsUnique = true
if in.NewId == 0 {
return
}
if err = hgorm.IsUnique(ctx, &dao.SysProvinces, g.Map{dao.SysProvinces.Columns().Id: in.NewId}, "", in.OldId); err != nil {
res.IsUnique = false
return
}
return
}
// Select 省市区选项
func (s *sSysProvinces) Select(ctx context.Context, in *sysin.ProvincesSelectInp) (res *sysin.ProvincesSelectModel, err error) {
res = new(sysin.ProvincesSelectModel)
cols := dao.SysProvinces.Columns()
mod := dao.SysProvinces.Ctx(ctx).Fields(fmt.Sprintf("%s as value, %s as label, %s ", cols.Id, cols.Title, cols.Level)).Where(cols.Pid, in.Value)
if err = mod.OrderAsc(cols.Sort).OrderAsc(cols.Id).Scan(&res.List); err != nil {
err = gerror.Wrap(err, "获取省市区选项失败!")
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
}
// treeList 树状列表
func (s *sSysProvinces) treeList(pid int64, nodes []*entity.SysProvinces) (list []*sysin.ProvincesTree) {
list = make([]*sysin.ProvincesTree, 0)
for _, v := range nodes {
if v.Pid == pid {
item := new(sysin.ProvincesTree)
item.SysProvinces = *v
item.Label = v.Title
item.Value = v.Id
item.Key = v.Id
child := s.treeList(v.Id, nodes)
if len(child) > 0 {
item.Children = child
}
list = append(list, item)
}
}
return
}
+309
View File
@@ -0,0 +1,309 @@
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/encrypt"
"github.com/gogf/gf/v2/database/gdb"
"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/os/gtime"
"github.com/gogf/gf/v2/util/grand"
)
type sSysRechargeOrder struct{}
func NewSysRechargeOrder() *sSysRechargeOrder { return &sSysRechargeOrder{} }
func init() { service.RegisterSysRechargeOrder(NewSysRechargeOrder()) }
func (s *sSysRechargeOrder) model(ctx context.Context) *gdb.Model {
return handler.Model(dao.RechargeOrder.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysRechargeOrder) baseModel(ctx context.Context) *gdb.Model {
rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns()
return s.model(ctx).As("r").
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("r", rc.MemberId)).
LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("r", rc.MemberId)).
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("r", rc.MemberId)+" AND "+col("w", wc.Currency)+"='CNY'").
LeftJoin(dao.PaymentChannel.Table()+" c", col("c", cc.Id)+"="+col("r", rc.ChannelId))
}
func (s *sSysRechargeOrder) listModel(ctx context.Context) *gdb.Model {
rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns()
return s.baseModel(ctx).
Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt))
}
func (s *sSysRechargeOrder) filter(ctx context.Context, mod *gdb.Model, in *sysin.RechargeOrderListInp) (*gdb.Model, error) {
rc, mc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.PaymentChannel.Columns()
if in.RechargeNo != "" {
mod = mod.WhereLike(col("r", rc.RechargeNo), "%"+in.RechargeNo+"%")
}
if in.Username != "" {
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, err := encrypt.HashField(ctx, in.Mobile)
if err != nil {
return nil, err
}
mod = mod.Where(col("m", mc.MobileHash), hash)
}
if in.RechargeStatus > 0 {
mod = mod.Where(col("r", rc.RechargeStatus), in.RechargeStatus)
}
if in.ChannelId > 0 {
mod = mod.Where(col("r", rc.ChannelId), in.ChannelId)
}
if in.ChannelName != "" {
mod = mod.WhereLike(col("c", cc.Name), "%"+in.ChannelName+"%")
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(col("r", rc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1])
}
return mod, nil
}
func (s *sSysRechargeOrder) List(ctx context.Context, in *sysin.RechargeOrderListInp) (list []*sysin.RechargeOrderListModel, total int, err error) {
mod, err := s.filter(ctx, s.listModel(ctx), in)
if err != nil {
return nil, 0, err
}
err = mod.Page(in.Page, in.PerPage).OrderDesc(col("r", dao.RechargeOrder.Columns().Id)).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取充值订单失败")
}
return
}
func (s *sSysRechargeOrder) Summary(ctx context.Context, in *sysin.RechargeOrderListInp) (res *sysin.RechargeSummaryModel, err error) {
rc := dao.RechargeOrder.Columns()
mod, err := s.filter(ctx, s.baseModel(ctx).Fields(
"COUNT(DISTINCT "+col("r", rc.Id)+") total_count",
"COALESCE(SUM("+col("r", rc.RequestAmount)+"),0) total_amount",
fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) paid_count", col("r", rc.RechargeStatus), consts.RechargeSuccess, col("r", rc.Id)),
fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) paid_amount", col("r", rc.RechargeStatus), consts.RechargeSuccess, col("r", rc.PaidAmount)),
fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) unpaid_count", col("r", rc.RechargeStatus), consts.RechargeUnpaid, col("r", rc.Id)),
fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) unpaid_amount", col("r", rc.RechargeStatus), consts.RechargeUnpaid, col("r", rc.RequestAmount)),
fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) review_count", col("r", rc.RechargeStatus), consts.RechargeReview, col("r", rc.Id)),
fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) review_amount", col("r", rc.RechargeStatus), consts.RechargeReview, col("r", rc.RequestAmount)),
fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) rejected_count", col("r", rc.RechargeStatus), consts.RechargeRejected, col("r", rc.Id)),
fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) rejected_amount", col("r", rc.RechargeStatus), consts.RechargeRejected, col("r", rc.RequestAmount)),
), in)
if err != nil {
return nil, err
}
res = new(sysin.RechargeSummaryModel)
if err = mod.Scan(res); err != nil {
return nil, gerror.Wrap(err, "汇总充值订单失败")
}
return
}
func (s *sSysRechargeOrder) View(ctx context.Context, in *sysin.RechargeOrderViewInp) (res *sysin.RechargeOrderListModel, err error) {
err = s.listModel(ctx).Where(col("r", dao.RechargeOrder.Columns().Id), in.Id).Scan(&res)
if err != nil {
return nil, gerror.Wrap(err, "获取充值订单详情失败")
}
if res == nil {
return nil, gerror.New("充值订单不存在")
}
return
}
func (s *sSysRechargeOrder) Review(ctx context.Context, in *sysin.RechargeReviewInp) (res *sysin.FinanceApprovalModel, err error) {
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var order *entity.RechargeOrder
if scanErr := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&order); scanErr != nil {
return scanErr
}
if order == nil {
return gerror.New("充值订单不存在")
}
if order.RechargeStatus != consts.RechargeReview {
return gerror.New("充值单已处理,请刷新列表")
}
if in.Decision != consts.FinanceDecisionApprove && in.Decision != consts.FinanceDecisionReject {
return gerror.New("审核结论不合法")
}
if in.Decision == consts.FinanceDecisionApprove {
if order.VoucherAttachmentIds == nil && order.RechargeType == consts.RechargeTypeManual {
return gerror.New("人工充值必须提供打款凭证")
}
if in.PaidAmount != order.RequestAmount {
return gerror.New("实付金额与申请金额不一致,请驳回或先核对")
}
var member *entity.AppMember
if scanErr := handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(order.MemberId).Scan(&member); scanErr != nil {
return scanErr
}
if member == nil || member.MemberStatus != consts.AppMemberStatusNormal {
return gerror.New("会员账户状态异常,不能入账")
}
if in.ExternalNo != "" {
count, countErr := s.model(ctx).Where(dao.RechargeOrder.Columns().ExternalNo, in.ExternalNo).WhereNot(dao.RechargeOrder.Columns().Id, order.Id).Count()
if countErr != nil {
return countErr
}
if count > 0 {
return gerror.New("外部支付单号已存在,疑似重复订单")
}
}
}
rc := dao.RechargeOrder.Columns()
now := gtime.Now()
ledgerId := uint64(0)
status := consts.RechargeRejected
updateData := g.Map{
rc.RechargeStatus: status,
rc.ApprovalRequestId: 0,
rc.ReviewReasonCode: in.ReasonCode,
rc.ReviewReason: in.ReasonText,
rc.ReviewedAt: now,
rc.UpdatedBy: contexts.GetUserId(ctx),
}
if in.Decision == consts.FinanceDecisionApprove {
wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount
if scanErr := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(wc.MemberId, order.MemberId).Where(wc.Currency, order.Currency).LockUpdate().Scan(&account); scanErr != nil {
return scanErr
}
if account == nil || account.AccountStatus != 1 {
return gerror.New("会员钱包不存在或不可入账")
}
after := account.AvailableAmount + in.PaidAmount
lc := dao.WalletLedger.Columns()
result, insertErr := dao.WalletLedger.Ctx(ctx).
Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt).
Hook(hook.SaveTenant).
Data(g.Map{lc.LedgerNo: "WL" + gtime.Now().Format("YmdHis") + grand.S(7), lc.AccountId: account.Id, lc.MemberId: order.MemberId, lc.BizType: "recharge", lc.BizNo: order.RechargeNo, lc.EntryType: "available", lc.Direction: consts.WalletDirectionIncome, lc.Amount: in.PaidAmount, lc.BeforeAmount: account.AvailableAmount, lc.AfterAmount: after, lc.Currency: account.Currency, lc.IdempotencyKey: fmt.Sprintf("recharge:%d", order.Id), lc.Remark: in.ReasonText, lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: now}).
Insert()
if insertErr != nil {
return gerror.Wrap(insertErr, "写入充值台账失败")
}
id, _ := result.LastInsertId()
ledgerId = uint64(id)
walletUpdate, updateErr := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).
WherePri(account.Id).
Where(wc.Version, account.Version).
Data(g.Map{wc.AvailableAmount: after, wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(in.PaidAmount)), wc.Version: gdb.Raw(wc.Version + "+1")}).
Update()
if updateErr != nil {
return updateErr
}
rows, _ := walletUpdate.RowsAffected()
if rows != 1 {
return gerror.New("钱包余额已变化,请重试")
}
status = consts.RechargeSuccess
updateData[rc.RechargeStatus] = status
updateData[rc.PaidAmount] = in.PaidAmount
updateData[rc.ExternalNo] = in.ExternalNo
updateData[rc.LedgerId] = ledgerId
updateData[rc.PaidAt] = now
updateData[rc.CreditedAt] = now
}
if _, updateErr := s.model(ctx).WherePri(order.Id).Data(updateData).Update(); updateErr != nil {
return updateErr
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "recharge", Action: "recharge.review", TargetType: "recharge_order", TargetId: order.Id, Before: order, After: g.Map{"decision": in.Decision, "status": status, "ledgerId": ledgerId}, Reason: in.ReasonText})
})
if err != nil {
return nil, err
}
return &sysin.FinanceApprovalModel{}, nil
}
func (s *sSysRechargeOrder) Execute(ctx context.Context, in *sysin.FinanceExecuteInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var approval *entity.ApprovalRequest
if err := service.SysApproval().Model(ctx).WherePri(in.ApprovalId).LockUpdate().Scan(&approval); err != nil {
return err
}
if approval == nil || approval.ApprovalType != "recharge_review" {
return gerror.New("充值审批不存在")
}
if approval.ApprovalStatus != consts.ApprovalStatusApproved || approval.ExecutedAt != nil {
return gerror.New("审批尚未通过或已执行")
}
var input sysin.RechargeReviewInp
if err := approval.PayloadJson.Scan(&input); err != nil {
return gerror.Wrap(err, "解析充值审批数据失败")
}
var order *entity.RechargeOrder
if err := s.model(ctx).WherePri(input.Id).LockUpdate().Scan(&order); err != nil {
return err
}
if order == nil || order.ApprovalRequestId != approval.Id || order.RechargeStatus != consts.RechargeReview {
return gerror.New("充值订单状态已变化")
}
rc := dao.RechargeOrder.Columns()
now := gtime.Now()
ledgerId := uint64(0)
if input.Decision == consts.FinanceDecisionApprove {
wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount
if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(wc.MemberId, order.MemberId).Where(wc.Currency, order.Currency).LockUpdate().Scan(&account); err != nil {
return err
}
if account == nil || account.AccountStatus != 1 {
return gerror.New("会员钱包不存在或不可入账")
}
after := account.AvailableAmount + input.PaidAmount
lc := dao.WalletLedger.Columns()
result, err := dao.WalletLedger.Ctx(ctx).Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt).Hook(hook.SaveTenant).Data(g.Map{lc.LedgerNo: "WL" + gtime.Now().Format("YmdHis") + grand.S(7), lc.AccountId: account.Id, lc.MemberId: order.MemberId, lc.BizType: "recharge", lc.BizNo: order.RechargeNo, lc.EntryType: "available", lc.Direction: consts.WalletDirectionIncome, lc.Amount: input.PaidAmount, lc.BeforeAmount: account.AvailableAmount, lc.AfterAmount: after, lc.Currency: account.Currency, lc.IdempotencyKey: fmt.Sprintf("recharge:%d", order.Id), lc.Remark: input.ReasonText, lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: now}).Insert()
if err != nil {
return gerror.Wrap(err, "写入充值台账失败")
}
id, _ := result.LastInsertId()
ledgerId = uint64(id)
update, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(wc.Version, account.Version).Data(g.Map{wc.AvailableAmount: after, wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(input.PaidAmount)), wc.Version: gdb.Raw(wc.Version + "+1")}).Update()
if err != nil {
return err
}
rows, _ := update.RowsAffected()
if rows != 1 {
return gerror.New("钱包余额已变化,请重试")
}
_, err = s.model(ctx).WherePri(order.Id).Data(g.Map{rc.RechargeStatus: consts.RechargeSuccess, rc.PaidAmount: input.PaidAmount, rc.ExternalNo: input.ExternalNo, rc.LedgerId: ledgerId, rc.ReviewedAt: now, rc.CreditedAt: now, rc.UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return err
}
} else {
if _, err := s.model(ctx).WherePri(order.Id).Data(g.Map{rc.RechargeStatus: consts.RechargeRejected, rc.ReviewedAt: now, rc.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return err
}
}
if err := recordFinanceReview(ctx, approval, "recharge", order.Id, input.Decision, input.ReasonCode, input.ReasonText, order); err != nil {
return err
}
if err := markApprovalExecuted(ctx, approval); err != nil {
return err
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "recharge", Action: "recharge.review.execute", TargetType: "recharge_order", TargetId: order.Id, Before: order, After: g.Map{"decision": input.Decision, "ledgerId": ledgerId}, Reason: input.ReasonText, ApprovalNo: approval.ApprovalNo})
})
}
func recordFinanceReview(ctx context.Context, approval *entity.ApprovalRequest, bizType string, bizId uint64, decision int, reasonCode, reasonText string, snapshot interface{}) error {
c := dao.FinanceReview.Columns()
_, err := dao.FinanceReview.Ctx(ctx).Fields(c.ApprovalRequestId, c.BizType, c.BizId, c.ReviewStage, c.ReviewerId, c.Decision, c.ReasonCode, c.ReasonText, c.SnapshotJson).Hook(hook.SaveTenant).Data(g.Map{c.ApprovalRequestId: approval.Id, c.BizType: bizType, c.BizId: bizId, c.ReviewStage: 2, c.ReviewerId: contexts.GetUserId(ctx), c.Decision: decision, c.ReasonCode: reasonCode, c.ReasonText: reasonText, c.SnapshotJson: gjson.New(snapshot)}).Insert()
return gerror.Wrap(err, "记录财务审核失败")
}
func markApprovalExecuted(ctx context.Context, approval *entity.ApprovalRequest) error {
c := dao.ApprovalRequest.Columns()
_, err := service.SysApproval().Model(ctx).WherePri(approval.Id).Data(g.Map{c.ApprovalStatus: consts.ApprovalStatusExecuted, c.ExecutedAt: gtime.Now(), c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
return err
}
+193
View File
@@ -0,0 +1,193 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
// @AutoGenerate Version 2.7.6
package sys
import (
"context"
"fmt"
"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/util/gconv"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/convert"
"hotgo/utility/excel"
)
type sSysServeLicense struct{}
func NewSysServeLicense() *sSysServeLicense {
return &sSysServeLicense{}
}
func init() {
service.RegisterSysServeLicense(NewSysServeLicense())
}
// Model 服务许可证ORM模型
func (s *sSysServeLicense) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(dao.SysServeLicense.Ctx(ctx), option...)
}
// List 获取服务许可证列表
func (s *sSysServeLicense) List(ctx context.Context, in *sysin.ServeLicenseListInp) (list []*sysin.ServeLicenseListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 查询许可ID
if in.Id > 0 {
mod = mod.Where(dao.SysServeLicense.Columns().Id, in.Id)
}
// 查询分组
if in.Group != "" {
mod = mod.Where(dao.SysServeLicense.Columns().Group, in.Group)
}
// 查询许可名称
if in.Name != "" {
mod = mod.WhereLike(dao.SysServeLicense.Columns().Name, "%"+in.Name+"%")
}
// 查询应用ID
if in.Appid != "" {
mod = mod.Where(dao.SysServeLicense.Columns().Appid, in.Appid)
}
// 查询授权结束时间
if len(in.EndAt) == 2 {
mod = mod.WhereBetween(dao.SysServeLicense.Columns().EndAt, in.EndAt[0], in.EndAt[1])
}
// 查询状态
if in.Status > 0 {
mod = mod.Where(dao.SysServeLicense.Columns().Status, in.Status)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.SysServeLicense.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
totalCount, err = mod.Clone().Count()
if err != nil {
err = gerror.Wrap(err, "获取服务许可证数据行失败,请稍后重试!")
return
}
if totalCount == 0 {
return
}
if err = mod.Fields(sysin.ServeLicenseListModel{}).Page(in.Page, in.PerPage).OrderDesc(dao.SysServeLicense.Columns().Id).Scan(&list); err != nil {
err = gerror.Wrap(err, "获取服务许可证列表失败,请稍后重试!")
return
}
serv := service.TCPServer().Instance()
for _, v := range list {
v.Online = serv.GetAppIdOnline(v.Appid)
}
return
}
// Export 导出服务许可证
func (s *sSysServeLicense) Export(ctx context.Context, in *sysin.ServeLicenseListInp) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(sysin.ServeLicenseExportModel{})
if err != nil {
return
}
var (
fileName = "导出服务许可证-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, form.CalPageCount(totalCount, in.PerPage), in.Page, len(list))
exports []sysin.ServeLicenseExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增服务许可证
func (s *sSysServeLicense) Edit(ctx context.Context, in *sysin.ServeLicenseEditInp) (err error) {
// 验证'Appid'唯一
if err = hgorm.IsUnique(ctx, &dao.SysServeLicense, g.Map{dao.SysServeLicense.Columns().Appid: in.Appid}, "应用ID已存在", in.Id); err != nil {
return
}
// 修改
if in.Id > 0 {
if _, err = s.Model(ctx).Fields(sysin.ServeLicenseUpdateFields{}).WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改服务许可证失败,请稍后重试!")
}
return
}
// 新增
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).Fields(sysin.ServeLicenseInsertFields{}).Data(in).OmitEmptyData().Insert(); err != nil {
err = gerror.Wrap(err, "新增服务许可证失败,请稍后重试!")
}
return
}
// Delete 删除服务许可证
func (s *sSysServeLicense) Delete(ctx context.Context, in *sysin.ServeLicenseDeleteInp) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Delete(); err != nil {
err = gerror.Wrap(err, "删除服务许可证失败,请稍后重试!")
return
}
return
}
// View 获取服务许可证指定信息
func (s *sSysServeLicense) View(ctx context.Context, in *sysin.ServeLicenseViewInp) (res *sysin.ServeLicenseViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取服务许可证信息,请稍后重试!")
return
}
return
}
// Status 更新服务许可证状态
func (s *sSysServeLicense) Status(ctx context.Context, in *sysin.ServeLicenseStatusInp) (err error) {
update := g.Map{
dao.SysServeLicense.Columns().Status: in.Status,
}
if _, err = s.Model(ctx).WherePri(in.Id).Data(update).Update(); err != nil {
err = gerror.Wrap(err, "更新服务许可证状态失败,请稍后重试!")
return
}
return
}
// AssignRouter 分配服务许可证路由
func (s *sSysServeLicense) AssignRouter(ctx context.Context, in *sysin.ServeLicenseAssignRouterInp) (err error) {
update := g.Map{
dao.SysServeLicense.Columns().Routes: in.Routes,
}
if _, err = s.Model(ctx).WherePri(in.Id).Data(update).Update(); err != nil {
err = gerror.Wrap(err, "分配服务许可证路由失败,请稍后重试!")
return
}
return
}
+138
View File
@@ -0,0 +1,138 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 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/database/gdb"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/gmode"
"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"
"hotgo/internal/service"
"hotgo/utility/convert"
"hotgo/utility/excel"
)
type sSysServeLog struct{}
func NewSysServeLog() *sSysServeLog {
return &sSysServeLog{}
}
func init() {
service.RegisterSysServeLog(NewSysServeLog())
}
// Model 服务日志Orm模型
func (s *sSysServeLog) Model(ctx context.Context) *gdb.Model {
return dao.SysServeLog.Ctx(ctx)
}
// List 获取服务日志列表
func (s *sSysServeLog) List(ctx context.Context, in *sysin.ServeLogListInp) (list []*sysin.ServeLogListModel, totalCount int, err error) {
mod := dao.SysServeLog.Ctx(ctx)
// 查询链路ID
if in.TraceId != "" {
mod = mod.Where(dao.SysServeLog.Columns().TraceId, in.TraceId)
}
// 非生产环境,允许关键词查询日志
// 生成环境使用需谨慎,日志量大易产生慢日志
if !gmode.IsProduct() && in.Content != "" {
mod = mod.WhereLike(dao.SysServeLog.Columns().Content, "%"+in.Content+"%")
}
// 查询日志级别
if in.LevelFormat != "" {
mod = mod.WhereLike(dao.SysServeLog.Columns().LevelFormat, in.LevelFormat)
}
// 查询触发时间(ns)
if len(in.TriggerNs) == 2 {
mod = mod.WhereBetween(dao.SysServeLog.Columns().TriggerNs, in.TriggerNs[0], in.TriggerNs[1])
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.SysServeLog.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 关联表sysLog
mod = mod.LeftJoin(hgorm.GenJoinOnRelation(
dao.SysServeLog.Table(), dao.SysServeLog.Columns().TraceId, // 主表表名,关联条件
dao.SysLog.Table(), "sysLog", dao.SysLog.Columns().ReqId, // 关联表表名,别名,关联条件
)...)
totalCount, err = mod.Clone().Count()
if err != nil || totalCount == 0 {
return
}
// 关联表select
fields, err := hgorm.GenJoinSelect(ctx, sysin.ServeLogListModel{}, &dao.SysServeLog, []*hgorm.Join{
{Dao: &dao.SysLog, Alias: "sysLog"},
})
if err != nil {
return
}
err = mod.Fields(fields).Handler(handler.FilterAuth).Page(in.Page, in.PerPage).OrderDesc(dao.SysServeLog.Columns().Id).Scan(&list)
return
}
// Export 导出服务日志
func (s *sSysServeLog) Export(ctx context.Context, in *sysin.ServeLogListInp) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(sysin.ServeLogExportModel{})
if err != nil {
return
}
var (
fileName = "导出服务日志-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, form.CalPageCount(totalCount, in.PerPage), in.Page, len(list))
exports []sysin.ServeLogExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return err
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Delete 删除服务日志
func (s *sSysServeLog) Delete(ctx context.Context, in *sysin.ServeLogDeleteInp) (err error) {
_, err = dao.SysServeLog.Ctx(ctx).Where(dao.SysServeLog.Columns().Id, in.Id).Delete()
return
}
// View 获取服务日志指定信息
func (s *sSysServeLog) View(ctx context.Context, in *sysin.ServeLogViewInp) (res *sysin.ServeLogViewModel, err error) {
err = dao.SysServeLog.Ctx(ctx).Where(dao.SysServeLog.Columns().Id, in.Id).Scan(&res)
return
}
// RealWrite 真实写入
func (s *sSysServeLog) RealWrite(ctx context.Context, models entity.SysServeLog) (err error) {
_, err = dao.SysServeLog.Ctx(ctx).FieldsEx(dao.SysLog.Columns().Id).Data(models).OmitEmptyData().Insert()
return
}
+291
View File
@@ -0,0 +1,291 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"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/util/grand"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/location"
"hotgo/internal/library/sms"
"hotgo/internal/model"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"time"
)
type sSysSmsLog struct{}
func NewSysSmsLog() *sSysSmsLog {
return &sSysSmsLog{}
}
func init() {
service.RegisterSysSmsLog(NewSysSmsLog())
}
// Delete 删除
func (s *sSysSmsLog) Delete(ctx context.Context, in *sysin.SmsLogDeleteInp) (err error) {
_, err = dao.SysSmsLog.Ctx(ctx).WherePri(in.Id).Delete()
return
}
// View 获取指定字典类型信息
func (s *sSysSmsLog) View(ctx context.Context, in *sysin.SmsLogViewInp) (res *sysin.SmsLogViewModel, err error) {
if err = dao.SysSmsLog.Ctx(ctx).WherePri(in.Id).Scan(&res); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
return
}
// List 获取列表
func (s *sSysSmsLog) List(ctx context.Context, in *sysin.SmsLogListInp) (list []*sysin.SmsLogListModel, totalCount int, err error) {
mod := dao.SysSmsLog.Ctx(ctx)
cols := dao.SysSmsLog.Columns()
if in.Mobile != "" {
mod = mod.WhereLike(cols.Mobile, "%"+in.Mobile+"%")
}
if in.Ip != "" {
mod = mod.Where(cols.Ip, in.Ip)
}
if in.Event != "" {
mod = mod.Where(cols.Event, in.Event)
}
if in.Status > 0 {
mod = mod.Where(cols.Status, in.Status)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(cols.CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
totalCount, err = mod.Count()
if err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
if totalCount == 0 {
return
}
if err = mod.Page(in.Page, in.PerPage).OrderDesc(cols.Id).Scan(&list); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
return
}
// SendCode 发送验证码
func (s *sSysSmsLog) SendCode(ctx context.Context, in *sysin.SendCodeInp) (err error) {
if in.Event == "" {
err = gerror.New("事件不能为空")
return
}
if in.Mobile == "" {
err = gerror.New("手机号不能为空")
return
}
var models *entity.SysSmsLog
if err = dao.SysSmsLog.Ctx(ctx).
Where(dao.SysSmsLog.Columns().Event, in.Event).
Where(dao.SysSmsLog.Columns().Mobile, in.Mobile).
OrderDesc(dao.SysSmsLog.Columns().Id).
Scan(&models); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
config, err := service.SysConfig().GetSms(ctx)
if err != nil {
return
}
if in.Template, err = s.GetTemplate(ctx, in.Event, config); err != nil {
return
}
if err = s.AllowSend(ctx, models, config); err != nil {
return
}
if in.Code == "" {
in.Code = grand.Digits(4)
}
if err = sms.New(config.SmsDrive).SendCode(ctx, in); err != nil {
return
}
var data = new(entity.SysSmsLog)
data.Event = in.Event
data.Mobile = in.Mobile
data.Code = in.Code
data.Ip = location.GetClientIp(ghttp.RequestFromCtx(ctx))
data.Status = consts.CodeStatusNotUsed
data.CreatedAt = gtime.Now()
data.UpdatedAt = gtime.Now()
_, err = dao.SysSmsLog.Ctx(ctx).Data(data).OmitEmptyData().Insert()
return
}
// GetTemplate 获取指定短信模板
func (s *sSysSmsLog) GetTemplate(ctx context.Context, template string, config *model.SmsConfig) (val string, err error) {
if template == "" {
err = gerror.New("模板不能为空")
return
}
if config == nil {
config, err = service.SysConfig().GetSms(ctx)
if err != nil {
return
}
}
switch config.SmsDrive {
case consts.SmsDriveAliYun:
if len(config.AliYunTemplate) == 0 {
err = gerror.New("管理员还没有配置任何阿里云短信模板!")
return
}
for _, v := range config.AliYunTemplate {
if v.Key == template {
return v.Value, nil
}
}
case consts.SmsDriveTencent:
if len(config.TencentTemplate) == 0 {
err = gerror.New("管理员还没有配置任何腾讯云短信模板!")
return
}
for _, v := range config.TencentTemplate {
if v.Key == template {
return v.Value, nil
}
}
default:
err = gerror.Newf("暂不支持短信驱动:%v", config.SmsDrive)
return
}
return
}
// AllowSend 是否允许发送
func (s *sSysSmsLog) AllowSend(ctx context.Context, models *entity.SysSmsLog, config *model.SmsConfig) (err error) {
if models == nil {
return
}
if config == nil {
if config, err = service.SysConfig().GetSms(ctx); err != nil {
return
}
}
if gtime.Now().Before(models.CreatedAt.Add(time.Second * time.Duration(config.SmsMinInterval))) {
err = gerror.New("发送频繁,请稍后再试!")
return
}
if config.SmsMaxIpLimit > 0 {
count, err := s.NowDayIpSendCount(ctx, models.Event)
if err != nil {
return err
}
if count >= config.SmsMaxIpLimit {
err = gerror.New("今天发送短信过多,请次日后再试!")
return err
}
}
return
}
// NowDayIpSendCount 当天IP累计发送次数
func (s *sSysSmsLog) NowDayIpSendCount(ctx context.Context, event string) (count int, err error) {
return dao.SysSmsLog.Ctx(ctx).
Where("ip", location.GetClientIp(ghttp.RequestFromCtx(ctx))).
Where("event", event).
WhereGTE("created_at", gtime.Now().Format("Y-m-d")).
Count()
}
// VerifyCode 效验验证码
func (s *sSysSmsLog) VerifyCode(ctx context.Context, in *sysin.VerifyCodeInp) (err error) {
if in.Event == "" {
err = gerror.New("事件不能为空")
return
}
if in.Mobile == "" {
err = gerror.New("手机号不能为空")
return
}
config, err := service.SysConfig().GetSms(ctx)
if err != nil {
return
}
var models *entity.SysSmsLog
cols := dao.SysSmsLog.Columns()
if err = dao.SysSmsLog.Ctx(ctx).Where(cols.Event, in.Event).Where(cols.Mobile, in.Mobile).OrderDesc(cols.Id).Scan(&models); err != nil {
err = gerror.Wrap(err, consts.ErrorORM)
return
}
if models == nil {
err = gerror.New("验证码错误")
return
}
if models.Times >= 10 {
err = gerror.New("验证码错误次数过多,请重新发送!")
return
}
if in.Event != consts.SmsTemplateCode {
if models.Status == consts.CodeStatusUsed {
err = gerror.New("验证码已使用,请重新发送!")
return
}
}
if gtime.Now().After(models.CreatedAt.Add(time.Second * time.Duration(config.SmsCodeExpire))) {
err = gerror.New("验证码已过期,请重新发送")
return
}
if models.Code != in.Code {
_, _ = dao.SysSmsLog.Ctx(ctx).WherePri(models.Id).Increment(cols.Times, 1)
err = gerror.New("验证码错误!")
return
}
_, err = dao.SysSmsLog.Ctx(ctx).WherePri(models.Id).Data(g.Map{
cols.Times: models.Times + 1,
cols.Status: consts.CodeStatusUsed,
cols.UpdatedAt: gtime.Now(),
}).Update()
return
}
+207
View File
@@ -0,0 +1,207 @@
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"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/gtime"
"github.com/gogf/gf/v2/util/grand"
)
type sSysTask struct{}
func NewSysTask() *sSysTask { return &sSysTask{} }
func init() { service.RegisterSysTask(NewSysTask()) }
func (s *sSysTask) Model(ctx context.Context) *gdb.Model {
return handler.Model(dao.Task.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysTask) relationModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.TaskLevel.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysTask) listModel(ctx context.Context) *gdb.Model {
tc, hc := dao.Task.Columns(), dao.TaskHall.Columns()
return s.Model(ctx).As("t").LeftJoin(dao.TaskHall.Table()+" h", col("h", hc.Id)+"="+col("t", tc.HallId)).Fields("t.*", fmt.Sprintf("COALESCE(%s,'') hall_name", col("h", hc.Name)))
}
func (s *sSysTask) List(ctx context.Context, in *sysin.TaskListInp) (list []*sysin.TaskListModel, total int, err error) {
tc := dao.Task.Columns()
mod := s.listModel(ctx)
if in.Title != "" {
mod = mod.WhereLike(col("t", tc.Title), "%"+in.Title+"%")
}
if in.HallId > 0 {
mod = mod.Where(col("t", tc.HallId), in.HallId)
}
if in.PublishStatus > 0 {
mod = mod.Where(col("t", tc.PublishStatus), in.PublishStatus)
}
if in.LevelId > 0 {
mod = mod.WhereExists(s.relationModel(ctx).As("tl").Fields("1").Where(col("tl", dao.TaskLevel.Columns().TaskId)+"="+col("t", tc.Id)).Where(col("tl", dao.TaskLevel.Columns().LevelId), in.LevelId))
}
if err = mod.Page(in.Page, in.PerPage).OrderAsc(col("t", tc.Sort)).OrderDesc(col("t", tc.Id)).ScanAndCount(&list, &total, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取任务列表失败")
}
for _, row := range list {
if err = s.loadLevels(ctx, row); err != nil {
return nil, 0, err
}
count, e := handler.Model(dao.TradeOrder.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(dao.TradeOrder.Columns().TaskId, row.Id).Count()
if e != nil {
return nil, 0, e
}
row.Referenced = count > 0
}
return
}
func (s *sSysTask) View(ctx context.Context, in *sysin.TaskViewInp) (res *sysin.TaskListModel, err error) {
if err = s.listModel(ctx).Where(col("t", dao.Task.Columns().Id), in.Id).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取任务详情失败")
}
if res == nil {
return nil, gerror.New("任务不存在")
}
err = s.loadLevels(ctx, res)
return
}
func (s *sSysTask) loadLevels(ctx context.Context, row *sysin.TaskListModel) error {
rc, lc := dao.TaskLevel.Columns(), dao.AppMemberLevel.Columns()
type item struct {
Id uint64 `json:"id"`
Name string `json:"name"`
}
var rows []*item
if err := s.relationModel(ctx).As("r").LeftJoin(dao.AppMemberLevel.Table()+" l", col("l", lc.Id)+"="+col("r", rc.LevelId)).Fields(col("l", lc.Id), col("l", lc.Name)).Where(col("r", rc.TaskId), row.Id).OrderAsc(col("l", lc.Sort)).Scan(&rows); err != nil {
return gerror.Wrap(err, "获取任务等级失败")
}
row.LevelIds = []uint64{}
row.LevelNames = []string{}
for _, v := range rows {
row.LevelIds = append(row.LevelIds, v.Id)
row.LevelNames = append(row.LevelNames, v.Name)
}
return nil
}
func (s *sSysTask) Edit(ctx context.Context, in *sysin.TaskEditInp) (id uint64, err error) {
amount, err := parseCents(in.Commission)
if err != nil {
return 0, err
}
in.CommissionAmount = amount
hc := dao.TaskHall.Columns()
hallCount, err := handler.Model(dao.TaskHall.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(in.HallId).WhereIn(hc.PublishStatus, []int{consts.BizPublishPublished, consts.BizPublishOffline, consts.BizPublishDraft}).Count()
if err != nil {
return 0, err
}
if hallCount == 0 {
return 0, gerror.New("任务大厅不存在")
}
lc := dao.AppMemberLevel.Columns()
levelCount, err := handler.Model(dao.AppMemberLevel.Ctx(ctx), &handler.Option{FilterTenant: true}).WhereIn(lc.Id, in.LevelIds).Where(lc.Status, consts.StatusEnabled).Count()
if err != nil {
return 0, err
}
if levelCount != len(in.LevelIds) {
return 0, gerror.New("所选会员等级不存在或已停用")
}
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
c := dao.Task.Columns()
uid := contexts.GetUserId(ctx)
var before *entity.Task
if in.Id > 0 {
if e := s.Model(ctx).WherePri(in.Id).LockUpdate().Scan(&before); e != nil {
return e
}
if before == nil {
return gerror.New("任务不存在")
}
result, e := s.Model(ctx).WherePri(in.Id).Where(c.Version, in.Version).Fields(sysin.TaskUpdateFields{}).Data(g.Map{c.HallId: in.HallId, c.Title: in.Title, c.Description: in.Description, c.DetailHtml: in.DetailHtml, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.CommissionAmount: in.CommissionAmount, c.CommissionRate: in.CommissionRate, c.DailyLimit: in.DailyLimit, c.PerMemberLimit: in.PerMemberLimit, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.Sort: in.Sort, c.Remark: in.Remark, c.UpdatedBy: uid, c.Version: gdb.Raw(c.Version + "+1")}).Update()
if e != nil {
return gerror.Wrap(e, "更新任务失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return gerror.New("任务数据已更新,请刷新后重试")
}
id = in.Id
} else {
taskNo := "TK" + gtime.Now().Format("YmdHis") + grand.S(5)
result, e := s.Model(ctx).Fields(c.TaskNo, c.HallId, c.Title, c.Description, c.DetailHtml, c.CoverAttachmentId, c.CoverUrl, c.CommissionAmount, c.CommissionRate, c.DailyLimit, c.PerMemberLimit, c.StartAt, c.EndAt, c.PublishStatus, c.Sort, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(g.Map{c.TaskNo: taskNo, c.HallId: in.HallId, c.Title: in.Title, c.Description: in.Description, c.DetailHtml: in.DetailHtml, c.CoverAttachmentId: in.CoverAttachmentId, c.CoverUrl: in.CoverUrl, c.CommissionAmount: in.CommissionAmount, c.CommissionRate: in.CommissionRate, c.DailyLimit: in.DailyLimit, c.PerMemberLimit: in.PerMemberLimit, c.StartAt: in.StartAt, c.EndAt: in.EndAt, c.PublishStatus: consts.BizPublishDraft, c.Sort: in.Sort, c.Remark: in.Remark, c.CreatedBy: uid, c.UpdatedBy: uid}).Insert()
if e != nil {
return gerror.Wrap(e, "创建任务失败")
}
last, _ := result.LastInsertId()
id = uint64(last)
}
rc := dao.TaskLevel.Columns()
if _, e := s.relationModel(ctx).Where(rc.TaskId, id).Delete(); e != nil {
return e
}
for _, levelId := range in.LevelIds {
if _, e := dao.TaskLevel.Ctx(ctx).Fields(rc.TaskId, rc.LevelId).Hook(hook.SaveTenant).Data(g.Map{rc.TaskId: id, rc.LevelId: levelId}).Insert(); e != nil {
return gerror.Wrap(e, "保存任务等级失败")
}
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "task", Action: "task.edit", TargetType: "task", TargetId: id, Before: before, After: g.Map{"title": in.Title, "commissionAmount": in.CommissionAmount, "levelIds": in.LevelIds}})
})
return
}
func (s *sSysTask) Status(ctx context.Context, in *sysin.TaskStatusInp) error {
c := dao.Task.Columns()
if in.PublishStatus == consts.BizPublishPublished {
count, err := s.relationModel(ctx).Where(dao.TaskLevel.Columns().TaskId, in.Id).Count()
if err != nil {
return err
}
if count == 0 {
return gerror.New("任务至少绑定一个会员等级后才能发布")
}
}
result, err := s.Model(ctx).WherePri(in.Id).Where(c.Version, in.Version).Data(g.Map{c.PublishStatus: in.PublishStatus, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "更新任务状态失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return gerror.New("任务数据已更新,请刷新后重试")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "task", Action: "task.status", TargetType: "task", TargetId: in.Id, After: g.Map{"publishStatus": in.PublishStatus}})
}
func (s *sSysTask) Delete(ctx context.Context, in *sysin.TaskDeleteInp) error {
c := dao.Task.Columns()
var row *entity.Task
if err := s.Model(ctx).WherePri(in.Id).Scan(&row); err != nil {
return err
}
if row == nil {
return gerror.New("任务不存在")
}
count, err := handler.Model(dao.TradeOrder.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(dao.TradeOrder.Columns().TaskId, in.Id).Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("任务已被订单引用,只允许下线,不能删除")
}
if row.PublishStatus == consts.BizPublishPublished {
_, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{c.PublishStatus: consts.BizPublishOffline, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
return err
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if _, e := s.relationModel(ctx).Where(dao.TaskLevel.Columns().TaskId, in.Id).Delete(); e != nil {
return e
}
_, e := s.Model(ctx).WherePri(in.Id).Data(g.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx), c.UpdatedBy: contexts.GetUserId(ctx)}).Update()
return gerror.Wrap(e, "删除任务失败")
})
}
+236
View File
@@ -0,0 +1,236 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/dict"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"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/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysTaskHall struct{}
func NewSysTaskHall() *sSysTaskHall { return &sSysTaskHall{} }
func init() {
service.RegisterSysTaskHall(NewSysTaskHall())
dict.RegisterFunc("taskHallOption", "任务大厅选项", service.SysTaskHall().Option, true)
}
func (s *sSysTaskHall) model(ctx context.Context) *gdb.Model {
return handler.Model(dao.TaskHall.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysTaskHall) relationModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.TaskHallLevel.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysTaskHall) List(ctx context.Context, in *sysin.TaskHallListInp) (list []*sysin.TaskHallListModel, total int, err error) {
c := dao.TaskHall.Columns()
mod := s.model(ctx).Fields(c.Id, c.HallCode, c.Name, c.IconUrl, c.Summary, c.CommissionRate, c.RateUnit, c.ClientRoute, c.PublishStatus, c.Sort, c.Version, c.CreatedAt)
if in.Name != "" {
mod = mod.WhereLike(c.Name, "%"+in.Name+"%")
}
if in.PublishStatus > 0 {
mod = mod.Where(c.PublishStatus, in.PublishStatus)
}
err = mod.Page(in.Page, in.PerPage).OrderAsc(c.Sort).OrderDesc(c.Id).ScanAndCount(&list, &total, false)
if err != nil {
return nil, 0, gerror.Wrap(err, "获取任务大厅列表失败")
}
for _, row := range list {
if err = s.loadLevels(ctx, row); err != nil {
return nil, 0, err
}
}
return
}
func (s *sSysTaskHall) View(ctx context.Context, in *sysin.TaskHallViewInp) (res *sysin.TaskHallListModel, err error) {
c := dao.TaskHall.Columns()
if err = s.model(ctx).Fields(c.Id, c.HallCode, c.Name, c.IconAttachmentId, c.IconUrl, c.Summary, c.CommissionRate, c.RateUnit, c.ClientRoute, c.PublishStatus, c.Sort, c.Version, c.Remark, c.CreatedAt).WherePri(in.Id).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取任务大厅详情失败")
}
if res == nil {
return nil, gerror.New("任务大厅不存在")
}
err = s.loadLevels(ctx, res)
return
}
func (s *sSysTaskHall) loadLevels(ctx context.Context, row *sysin.TaskHallListModel) error {
rc, lc := dao.TaskHallLevel.Columns(), dao.AppMemberLevel.Columns()
type levelRow struct {
Id uint64 `json:"id"`
Name string `json:"name"`
}
var rows []*levelRow
err := s.relationModel(ctx).As("r").LeftJoin(dao.AppMemberLevel.Table()+" l", col("l", lc.Id)+"="+col("r", rc.LevelId)).Fields(col("l", lc.Id), col("l", lc.Name)).Where(col("r", rc.HallId), row.Id).OrderAsc(col("l", lc.Sort)).Scan(&rows)
if err != nil {
return gerror.Wrap(err, "获取大厅可用等级失败")
}
for _, v := range rows {
row.LevelIds = append(row.LevelIds, v.Id)
row.LevelNames = append(row.LevelNames, v.Name)
}
if row.LevelIds == nil {
row.LevelIds = []uint64{}
row.LevelNames = []string{}
}
return nil
}
func (s *sSysTaskHall) Edit(ctx context.Context, in *sysin.TaskHallEditInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
c := dao.TaskHall.Columns()
var before *entity.TaskHall
if in.Id > 0 {
if err := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&before); err != nil {
return err
}
if before == nil {
return gerror.New("任务大厅不存在")
}
if before.Version != in.Version {
return gerror.New("大厅数据已更新,请刷新后重试")
}
}
count, err := s.model(ctx).Where(c.HallCode, in.HallCode).WhereNot(c.Id, in.Id).Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("大厅编码已存在")
}
count, err = s.model(ctx).Where(c.Name, in.Name).WhereNot(c.Id, in.Id).Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("大厅名称已存在")
}
levelColumns := dao.AppMemberLevel.Columns()
var memberLevel *entity.AppMemberLevel
if err = handler.Model(dao.AppMemberLevel.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(in.LevelIds[0]).Where(levelColumns.Status, consts.StatusEnabled).WhereGT(levelColumns.CurrentVersionId, 0).Scan(&memberLevel); err != nil {
return gerror.Wrap(err, "获取会员等级失败")
}
if memberLevel == nil {
return gerror.New("所选会员等级不存在、未发布或已停用")
}
var levelVersion *entity.AppMemberLevelVersion
if err = handler.Model(dao.AppMemberLevelVersion.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(memberLevel.CurrentVersionId).Scan(&levelVersion); err != nil {
return gerror.Wrap(err, "获取会员等级比例失败")
}
if levelVersion == nil {
return gerror.New("所选会员等级没有已发布规则")
}
in.CommissionRate = levelVersion.CommissionRate
userId := contexts.GetUserId(ctx)
data := g.Map{c.HallCode: in.HallCode, c.Name: in.Name, c.IconAttachmentId: in.IconAttachmentId, c.IconUrl: in.IconUrl, c.Summary: in.Summary, c.CommissionRate: in.CommissionRate, c.RateUnit: "basis_point", c.ClientRoute: in.ClientRoute, c.Sort: in.Sort, c.Remark: in.Remark, c.UpdatedBy: userId}
if in.Id > 0 {
data[c.Version] = gdb.Raw(c.Version + "+1")
result, err := s.model(ctx).WherePri(in.Id).Where(c.Version, in.Version).Data(data).Update()
if err != nil {
return gerror.Wrap(err, "更新任务大厅失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return gerror.New("大厅数据已更新,请刷新后重试")
}
} else {
data[c.PublishStatus] = consts.BizPublishPublished
data[c.CreatedBy] = userId
result, err := s.model(ctx).Fields(c.HallCode, c.Name, c.IconAttachmentId, c.IconUrl, c.Summary, c.CommissionRate, c.RateUnit, c.ClientRoute, c.PublishStatus, c.Sort, c.Remark, c.CreatedBy, c.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
if err != nil {
return gerror.Wrap(err, "创建任务大厅失败")
}
last, _ := result.LastInsertId()
in.Id = uint64(last)
}
rc := dao.TaskHallLevel.Columns()
if _, err = s.relationModel(ctx).Where(rc.HallId, in.Id).Delete(); err != nil {
return gerror.Wrap(err, "更新大厅等级关系失败")
}
for _, levelId := range in.LevelIds {
if _, err = dao.TaskHallLevel.Ctx(ctx).Fields(rc.HallId, rc.LevelId).Hook(hook.SaveTenant).Data(g.Map{rc.HallId: in.Id, rc.LevelId: levelId}).Insert(); err != nil {
return gerror.Wrap(err, "保存大厅等级关系失败")
}
}
_ = dict.ClearFuncCache("taskHallOption")
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "task_hall", Action: "task_hall.edit", TargetType: "task_hall", TargetId: in.Id, Before: before, After: g.Map{"name": in.Name, "commissionRate": in.CommissionRate, "levelIds": in.LevelIds, "clientRoute": in.ClientRoute}, Reason: in.Remark})
})
}
func (s *sSysTaskHall) Status(ctx context.Context, in *sysin.TaskHallStatusInp) error {
c := dao.TaskHall.Columns()
var row *entity.TaskHall
if err := s.model(ctx).WherePri(in.Id).Scan(&row); err != nil {
return err
}
if row == nil {
return gerror.New("任务大厅不存在")
}
if row.Version != in.Version {
return gerror.New("大厅数据已更新,请刷新后重试")
}
if in.PublishStatus == consts.BizPublishPublished {
count, err := s.relationModel(ctx).Where(dao.TaskHallLevel.Columns().HallId, in.Id).Count()
if err != nil {
return err
}
if count == 0 {
return gerror.New("任务大厅至少绑定一个会员等级后才能发布")
}
}
result, err := s.model(ctx).WherePri(in.Id).Where(c.Version, in.Version).Data(g.Map{c.PublishStatus: in.PublishStatus, c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "更新任务大厅状态失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return gerror.New("大厅数据已更新,请刷新后重试")
}
_ = dict.ClearFuncCache("taskHallOption")
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "task_hall", Action: "task_hall.status", TargetType: "task_hall", TargetId: in.Id, Before: g.Map{"publishStatus": row.PublishStatus}, After: g.Map{"publishStatus": in.PublishStatus}})
}
func (s *sSysTaskHall) Delete(ctx context.Context, in *sysin.TaskHallDeleteInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
c := dao.TaskHall.Columns()
var row *entity.TaskHall
if err := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&row); err != nil {
return err
}
if row == nil {
return gerror.New("任务大厅不存在")
}
rc := dao.TaskHallLevel.Columns()
if _, err := s.relationModel(ctx).Where(rc.HallId, in.Id).Delete(); err != nil {
return err
}
if _, err := s.model(ctx).WherePri(in.Id).Data(g.Map{c.DeletedAt: gtime.Now(), c.DeletedBy: contexts.GetUserId(ctx), c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return gerror.Wrap(err, "删除任务大厅失败")
}
_ = dict.ClearFuncCache("taskHallOption")
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "task_hall", Action: "task_hall.delete", TargetType: "task_hall", TargetId: in.Id, Before: row, Reason: in.Reason})
})
}
func (s *sSysTaskHall) Option(ctx context.Context) (opts []*model.Option, err error) {
var rows []*entity.TaskHall
c := dao.TaskHall.Columns()
if err = s.model(ctx).Fields(c.Id, c.Name).Where(c.PublishStatus, consts.BizPublishPublished).OrderAsc(c.Sort).OrderAsc(c.Id).Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "获取任务大厅选项失败")
}
opts = make([]*model.Option, len(rows))
for i, row := range rows {
opts[i] = dict.GenHashOption(row.Id, gconv.String(row.Name))
}
return
}
@@ -0,0 +1,153 @@
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/encrypt"
"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/gtime"
"github.com/gogf/gf/v2/util/grand"
)
type sSysTaskSubmission struct{}
func NewSysTaskSubmission() *sSysTaskSubmission { return &sSysTaskSubmission{} }
func init() { service.RegisterSysTaskSubmission(NewSysTaskSubmission()) }
func (s *sSysTaskSubmission) Model(ctx context.Context) *gdb.Model {
return handler.Model(dao.TaskSubmission.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysTaskSubmission) listModel(ctx context.Context) *gdb.Model {
sc, tc, mc, lc := dao.TaskSubmission.Columns(), dao.Task.Columns(), dao.AppMember.Columns(), dao.AppMemberLevel.Columns()
return s.Model(ctx).As("s").LeftJoin(dao.Task.Table()+" t", col("t", tc.Id)+"="+col("s", sc.TaskId)).LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("s", sc.MemberId)).LeftJoin(dao.AppMemberLevel.Table()+" l", col("l", lc.Id)+"="+col("s", sc.MemberLevelId)).Fields(col("s", sc.Id), col("s", sc.SubmissionNo), col("s", sc.TradeOrderId), col("s", sc.TaskId), fmt.Sprintf("COALESCE(%s,'') task_title", col("t", tc.Title)), col("s", sc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), fmt.Sprintf("COALESCE(%s,'') level_name", col("l", lc.Name)), col("s", sc.EvidenceText), col("s", sc.EvidenceAttachmentIds), col("s", sc.SubmissionStatus), col("s", sc.CommissionAmount), col("s", sc.SuccessRate), col("s", sc.ReviewReasonCode), col("s", sc.ReviewReason), col("s", sc.AllowResubmit), col("s", sc.LedgerId), col("s", sc.ReviewedAt), col("s", sc.SettledAt), col("s", sc.CreatedAt))
}
func (s *sSysTaskSubmission) filter(ctx context.Context, mod *gdb.Model, in *sysin.TaskSubmissionListInp) (*gdb.Model, error) {
sc, mc := dao.TaskSubmission.Columns(), dao.AppMember.Columns()
if in.Username != "" {
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, err := encrypt.HashField(ctx, in.Mobile)
if err != nil {
return nil, err
}
mod = mod.Where(col("m", mc.MobileHash), hash)
}
if in.SubmissionStatus > 0 {
mod = mod.Where(col("s", sc.SubmissionStatus), in.SubmissionStatus)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(col("s", sc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1])
}
return mod, nil
}
func (s *sSysTaskSubmission) List(ctx context.Context, in *sysin.TaskSubmissionListInp) (list []*sysin.TaskSubmissionListModel, total int, err error) {
mod, err := s.filter(ctx, s.listModel(ctx), in)
if err != nil {
return nil, 0, err
}
if err = mod.Page(in.Page, in.PerPage).OrderDesc(col("s", dao.TaskSubmission.Columns().Id)).ScanAndCount(&list, &total, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取会员任务审核列表失败")
}
return
}
func (s *sSysTaskSubmission) View(ctx context.Context, in *sysin.TaskSubmissionViewInp) (res *sysin.TaskSubmissionListModel, err error) {
if err = s.listModel(ctx).Where(col("s", dao.TaskSubmission.Columns().Id), in.Id).Scan(&res); err != nil {
return nil, gerror.Wrap(err, "获取任务提交详情失败")
}
if res == nil {
return nil, gerror.New("任务提交不存在")
}
return
}
func (s *sSysTaskSubmission) Review(ctx context.Context, in *sysin.TaskSubmissionReviewInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
sc := dao.TaskSubmission.Columns()
var submission *entity.TaskSubmission
if err := s.Model(ctx).WherePri(in.Id).LockUpdate().Scan(&submission); err != nil {
return err
}
if submission == nil {
return gerror.New("任务提交不存在")
}
if submission.SubmissionStatus != consts.TaskSubmissionPending {
return gerror.New("该记录已审核,请勿重复操作")
}
var task *entity.Task
if err := handler.Model(dao.Task.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(submission.TaskId).Scan(&task); err != nil {
return err
}
if task == nil {
return gerror.New("关联任务不存在")
}
var member *entity.AppMember
if err := handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(submission.MemberId).Scan(&member); err != nil {
return err
}
if member == nil || member.MemberStatus != consts.AppMemberStatusNormal {
return gerror.New("会员不存在或状态异常")
}
now := gtime.Now()
uid := contexts.GetUserId(ctx)
if in.Decision == consts.FinanceDecisionReject {
_, err := s.Model(ctx).WherePri(in.Id).Data(g.Map{sc.SubmissionStatus: consts.TaskSubmissionRejected, sc.ReviewedBy: uid, sc.ReviewedAt: now, sc.ReviewReasonCode: in.ReasonCode, sc.ReviewReason: in.ReasonText, sc.AllowResubmit: in.AllowResubmit, sc.UpdatedBy: uid}).Update()
if err != nil {
return gerror.Wrap(err, "驳回任务提交失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "task_submission", Action: "task_submission.reject", TargetType: "task_submission", TargetId: submission.Id, Before: submission, After: g.Map{"status": consts.TaskSubmissionRejected, "allowResubmit": in.AllowResubmit}, Reason: in.ReasonText})
}
wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount
if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(wc.MemberId, submission.MemberId).Where(wc.Currency, "CNY").LockUpdate().Scan(&account); err != nil {
return err
}
if account == nil || account.AccountStatus != 1 {
return gerror.New("会员钱包不存在或不可结算")
}
lc := dao.WalletLedger.Columns()
count, err := handler.Model(dao.WalletLedger.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(lc.IdempotencyKey, in.IdempotencyKey).Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("该幂等键已使用,任务佣金未重复发放")
}
after := account.AvailableAmount + submission.CommissionAmount
result, err := dao.WalletLedger.Ctx(ctx).Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt).Hook(hook.SaveTenant).Data(g.Map{lc.LedgerNo: "WL" + gtime.Now().Format("YmdHis") + grand.S(7), lc.AccountId: account.Id, lc.MemberId: submission.MemberId, lc.BizType: "commission", lc.BizNo: submission.SubmissionNo, lc.EntryType: "available", lc.Direction: consts.WalletDirectionIncome, lc.Amount: submission.CommissionAmount, lc.BeforeAmount: account.AvailableAmount, lc.AfterAmount: after, lc.Currency: account.Currency, lc.IdempotencyKey: in.IdempotencyKey, lc.Remark: in.ReasonText, lc.OperatorId: uid, lc.OccurredAt: now}).Insert()
if err != nil {
return gerror.Wrap(err, "写入任务佣金台账失败")
}
ledgerId64, _ := result.LastInsertId()
update, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(wc.Version, account.Version).Data(g.Map{wc.AvailableAmount: after, wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(submission.CommissionAmount)), wc.Version: gdb.Raw(wc.Version + "+1")}).Update()
if err != nil {
return err
}
rows, _ := update.RowsAffected()
if rows != 1 {
return gerror.New("钱包余额已变化,请重试")
}
_, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{sc.SubmissionStatus: consts.TaskSubmissionSettled, sc.ReviewedBy: uid, sc.ReviewedAt: now, sc.ReviewReasonCode: in.ReasonCode, sc.ReviewReason: in.ReasonText, sc.AllowResubmit: consts.StatusDisable, sc.LedgerId: uint64(ledgerId64), sc.SettledAt: now, sc.UpdatedBy: uid}).Update()
if err != nil {
return err
}
if submission.TradeOrderId > 0 {
tc := dao.TradeOrder.Columns()
_, err = handler.Model(dao.TradeOrder.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(submission.TradeOrderId).Data(g.Map{tc.OrderStatus: consts.TradeOrderCompleted, tc.CompletedAt: now, tc.UpdatedBy: uid}).Update()
if err != nil {
return err
}
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "task_submission", Action: "task_submission.settle", TargetType: "task_submission", TargetId: submission.Id, Before: submission, After: g.Map{"status": consts.TaskSubmissionSettled, "ledgerId": uint64(ledgerId64), "commissionAmount": submission.CommissionAmount}, Reason: in.ReasonText})
})
}
+167
View File
@@ -0,0 +1,167 @@
// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2024 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
// @AutoGenerate Version 2.15.7
package sys
import (
"context"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"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/util/gconv"
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
type sSysTestCategory struct{}
func NewSysTestCategory() *sSysTestCategory {
return &sSysTestCategory{}
}
func init() {
service.RegisterSysTestCategory(NewSysTestCategory())
dict.RegisterFunc("testCategoryOption", "测试分类选项", service.SysTestCategory().Option)
}
// Model 测试分类ORM模型
func (s *sSysTestCategory) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(dao.TestCategory.Ctx(ctx), option...)
}
// List 获取测试分类列表
func (s *sSysTestCategory) List(ctx context.Context, in *sysin.TestCategoryListInp) (list []*sysin.TestCategoryListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(sysin.TestCategoryListModel{})
// 查询分类ID
if in.Id > 0 {
mod = mod.Where(dao.TestCategory.Columns().Id, in.Id)
}
// 查询分类名称
if in.Name != "" {
mod = mod.WhereLike(dao.TestCategory.Columns().Name, "%"+in.Name+"%")
}
// 查询状态
if in.Status > 0 {
mod = mod.Where(dao.TestCategory.Columns().Status, in.Status)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TestCategory.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PerPage)
// 排序
mod = mod.OrderAsc(dao.TestCategory.Columns().Sort).OrderDesc(dao.TestCategory.Columns().Id)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取测试分类列表失败,请稍后重试!")
return
}
return
}
// Edit 修改/新增测试分类
func (s *sSysTestCategory) Edit(ctx context.Context, in *sysin.TestCategoryEditInp) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
if _, err = s.Model(ctx).
Fields(sysin.TestCategoryUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改测试分类失败,请稍后重试!")
}
return
}
// 新增
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(sysin.TestCategoryInsertFields{}).
Data(in).OmitEmptyData().Insert(); err != nil {
err = gerror.Wrap(err, "新增测试分类失败,请稍后重试!")
}
return
})
}
// Delete 删除测试分类
func (s *sSysTestCategory) Delete(ctx context.Context, in *sysin.TestCategoryDeleteInp) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除测试分类失败,请稍后重试!")
return
}
return
}
// MaxSort 获取测试分类最大排序
func (s *sSysTestCategory) MaxSort(ctx context.Context, in *sysin.TestCategoryMaxSortInp) (res *sysin.TestCategoryMaxSortModel, err error) {
if err = dao.TestCategory.Ctx(ctx).Fields(dao.TestCategory.Columns().Sort).OrderDesc(dao.TestCategory.Columns().Sort).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取测试分类最大排序,请稍后重试!")
return
}
if res == nil {
res = new(sysin.TestCategoryMaxSortModel)
}
res.Sort = form.DefaultMaxSort(res.Sort)
return
}
// View 获取测试分类指定信息
func (s *sSysTestCategory) View(ctx context.Context, in *sysin.TestCategoryViewInp) (res *sysin.TestCategoryViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取测试分类信息,请稍后重试!")
return
}
return
}
// Status 更新测试分类状态
func (s *sSysTestCategory) Status(ctx context.Context, in *sysin.TestCategoryStatusInp) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.TestCategory.Columns().Status: in.Status,
}).Update(); err != nil {
err = gerror.Wrap(err, "更新测试分类状态失败,请稍后重试!")
return
}
return
}
// Option 获取测试分类选项
func (s *sSysTestCategory) Option(ctx context.Context) (opts []*model.Option, err error) {
var models []*entity.TestCategory
if err = s.Model(ctx).Fields(dao.TestCategory.Columns().Id, dao.TestCategory.Columns().Name).
OrderAsc(dao.TestCategory.Columns().Sort).OrderDesc(dao.TestCategory.Columns().Id).Scan(&models); err != nil {
err = gerror.Wrap(err, "获取测试分类选项失败!")
return
}
opts = make([]*model.Option, len(models))
for k, v := range models {
opts[k] = dict.GenHashOption(v.Id, gconv.String(v.Name))
}
return
}
+316
View File
@@ -0,0 +1,316 @@
package sys
import (
"context"
"fmt"
"math"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/encrypt"
"github.com/gogf/gf/v2/database/gdb"
"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/os/gtime"
)
type sSysTradeOrder struct{}
func NewSysTradeOrder() *sSysTradeOrder { return &sSysTradeOrder{} }
func init() { service.RegisterSysTradeOrder(NewSysTradeOrder()) }
func (s *sSysTradeOrder) model(ctx context.Context) *gdb.Model {
return handler.Model(dao.TradeOrder.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysTradeOrder) listModel(ctx context.Context) *gdb.Model {
oc, mc, pc, wc, ac := dao.TradeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.ApprovalRequest.Columns()
todayCount := fmt.Sprintf("(SELECT COUNT(*) FROM %s td WHERE td.%s=%s AND td.%s>=CURDATE()) today_trade_count", dao.TradeOrder.Table(), oc.MemberId, col("o", oc.MemberId), oc.CreatedAt)
return s.model(ctx).As("o").
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("o", oc.MemberId)).
LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("o", oc.MemberId)).
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("o", oc.MemberId)+" AND "+col("w", wc.Currency)+"='CNY'").
LeftJoin(dao.ApprovalRequest.Table()+" a", col("a", ac.Id)+"="+col("o", oc.ApprovalRequestId)).
Fields(col("o", oc.Id), col("o", oc.OrderNo), col("o", oc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), col("m", mc.AutoDispatch), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), todayCount, col("o", oc.ProductName), col("o", oc.OrderType), col("o", oc.Quantity), col("o", oc.UnitPriceAmount), col("o", oc.TotalAmount), col("o", oc.CommissionAmount), col("o", oc.Currency), col("o", oc.Message), col("o", oc.OrderStatus), col("o", oc.FreezeStatus), col("o", oc.ApprovalRequestId), fmt.Sprintf("COALESCE(%s,0) approval_status", col("a", ac.ApprovalStatus)), col("o", oc.MatchedAt), col("o", oc.CompletedAt), col("o", oc.UnfrozenAt), col("o", oc.CreatedAt))
}
func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp) (list []*sysin.TradeOrderListModel, total int, err error) {
oc, mc := dao.TradeOrder.Columns(), dao.AppMember.Columns()
mod := s.listModel(ctx)
if in.OrderNo != "" {
mod = mod.WhereLike(col("o", oc.OrderNo), "%"+in.OrderNo+"%")
}
if in.Username != "" {
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, e := encrypt.HashField(ctx, in.Mobile)
if e != nil {
return nil, 0, e
}
mod = mod.Where(col("m", mc.MobileHash), hash)
}
if in.OrderStatus > 0 {
mod = mod.Where(col("o", oc.OrderStatus), in.OrderStatus)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(col("o", oc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1])
}
matchFirst := fmt.Sprintf("CASE WHEN %s=%d THEN 0 ELSE 1 END", col("o", oc.OrderStatus), consts.TradeOrderMatching)
err = mod.Page(in.Page, in.PerPage).OrderAsc(matchFirst).OrderDesc(col("o", oc.Id)).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取交易订单失败")
return
}
for _, row := range list {
row.CanMatch = row.OrderStatus == consts.TradeOrderMatching && row.ApprovalRequestId == 0
row.CanApply = row.ApprovalRequestId == 0 && row.OrderStatus != consts.TradeOrderCompleted && row.OrderStatus != consts.TradeOrderCancelled
row.CanExecute = row.ApprovalRequestId > 0 && row.ApprovalStatus == consts.ApprovalStatusApproved
}
return
}
func (s *sSysTradeOrder) View(ctx context.Context, in *sysin.TradeOrderViewInp) (res *sysin.TradeOrderViewModel, err error) {
var base *sysin.TradeOrderListModel
if err = s.listModel(ctx).Where(col("o", dao.TradeOrder.Columns().Id), in.Id).Scan(&base); err != nil {
return nil, gerror.Wrap(err, "获取订单详情失败")
}
if base == nil {
return nil, gerror.New("交易订单不存在")
}
var order *entity.TradeOrder
if err = s.model(ctx).WherePri(in.Id).Scan(&order); err != nil {
return nil, err
}
res = &sysin.TradeOrderViewModel{TradeOrderListModel: *base, TaskId: order.TaskId, ProductId: order.ProductId, MemberLevelId: order.MemberLevelId, RuleSnapshot: order.RuleSnapshot}
return
}
func (s *sSysTradeOrder) MatchProducts(ctx context.Context, in *sysin.TradeOrderMatchProductListInp) (list []*sysin.TradeOrderMatchProductModel, total int, err error) {
pc := dao.Product.Columns()
mod := handler.Model(dao.Product.Ctx(ctx), &handler.Option{FilterTenant: true}).
Fields(pc.Id, pc.Name, pc.CoverUrl, pc.PriceAmount).
Where(pc.PublishStatus, consts.BizPublishPublished).
WhereNull(pc.DeletedAt).
Where("("+pc.StartAt+" IS NULL OR "+pc.StartAt+" <= ?)", gtime.Now()).
Where("("+pc.EndAt+" IS NULL OR "+pc.EndAt+" >= ?)", gtime.Now())
if in.Name != "" {
mod = mod.WhereLike(pc.Name, "%"+in.Name+"%")
}
if in.PriceMin != "" {
amount, parseErr := parseCents(in.PriceMin)
if parseErr != nil {
return nil, 0, gerror.New("最低价格格式不正确")
}
mod = mod.WhereGTE(pc.PriceAmount, amount)
}
if in.PriceMax != "" {
amount, parseErr := parseCents(in.PriceMax)
if parseErr != nil {
return nil, 0, gerror.New("最高价格格式不正确")
}
mod = mod.WhereLTE(pc.PriceAmount, amount)
}
if in.PriceMin != "" && in.PriceMax != "" {
minAmount, _ := parseCents(in.PriceMin)
maxAmount, _ := parseCents(in.PriceMax)
if minAmount > maxAmount {
return nil, 0, gerror.New("最低价格不能高于最高价格")
}
}
err = mod.Page(in.Page, in.PerPage).OrderDesc(pc.PriceAmount).OrderDesc(pc.Id).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取可匹配商品失败")
}
return
}
func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var order *entity.TradeOrder
if err := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&order); err != nil {
return gerror.Wrap(err, "读取待匹配订单失败")
}
if order == nil {
return gerror.New("交易订单不存在")
}
if order.OrderStatus != consts.TradeOrderMatching || order.ApprovalRequestId > 0 {
return gerror.New("当前订单状态不允许匹配商品")
}
var product *entity.Product
pc := dao.Product.Columns()
if err := handler.Model(dao.Product.Ctx(ctx), &handler.Option{FilterTenant: true}).
WherePri(in.ProductId).Where(pc.PublishStatus, consts.BizPublishPublished).WhereNull(pc.DeletedAt).
LockUpdate().Scan(&product); err != nil {
return gerror.Wrap(err, "读取匹配商品失败")
}
if product == nil {
return gerror.New("商品不存在、已下架或无权使用")
}
now := gtime.Now()
if product.StartAt != nil && product.StartAt.After(now) || product.EndAt != nil && product.EndAt.Before(now) {
return gerror.New("商品当前不在可匹配时间范围内")
}
if product.PriceAmount <= 0 || product.PriceAmount > math.MaxInt64/int64(in.Quantity) {
return gerror.New("商品价格或数量不合法")
}
levelId := order.MemberLevelId
if levelId == 0 {
var member *entity.AppMember
if err := handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).Fields(dao.AppMember.Columns().LevelId).WherePri(order.MemberId).Scan(&member); err != nil {
return gerror.Wrap(err, "读取会员等级失败")
}
if member == nil {
return gerror.New("订单会员不存在")
}
levelId = member.LevelId
}
var level *entity.AppMemberLevel
if err := handler.Model(dao.AppMemberLevel.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(levelId).Scan(&level); err != nil {
return gerror.Wrap(err, "读取会员等级规则失败")
}
if level == nil || level.CurrentVersionId == 0 {
return gerror.New("会员等级尚未配置生效规则")
}
var levelVersion *entity.AppMemberLevelVersion
if err := handler.Model(dao.AppMemberLevelVersion.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(level.CurrentVersionId).Scan(&levelVersion); err != nil {
return gerror.Wrap(err, "读取会员等级版本失败")
}
if levelVersion == nil {
return gerror.New("会员等级生效版本不存在")
}
totalAmount := product.PriceAmount * int64(in.Quantity)
commissionAmount := totalAmount * int64(levelVersion.CommissionRate) / 10000
snapshot := gjson.New(g.Map{
"source": "admin_manual_match", "productId": product.Id, "productNo": product.ProductNo,
"unitPriceAmount": product.PriceAmount, "quantity": in.Quantity,
"memberLevelId": levelId, "levelVersionId": levelVersion.Id,
"commissionRate": levelVersion.CommissionRate,
})
oc := dao.TradeOrder.Columns()
data := g.Map{
oc.ProductId: product.Id, oc.ProductName: product.Name, oc.OrderType: consts.TradeOrderTypeManual,
oc.Quantity: in.Quantity, oc.UnitPriceAmount: product.PriceAmount, oc.TotalAmount: totalAmount,
oc.CommissionAmount: commissionAmount, oc.Currency: product.Currency, oc.MemberLevelId: levelId,
oc.RuleSnapshot: snapshot, oc.Message: "后台人工匹配商品", oc.OrderStatus: consts.TradeOrderPending,
oc.FreezeStatus: consts.TradeFreezeNormal, oc.MatchedAt: now, oc.UpdatedBy: contexts.GetUserId(ctx),
}
result, err := s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderMatching).Data(data).Update()
if err != nil {
return gerror.Wrap(err, "匹配订单失败")
}
rows, _ := result.RowsAffected()
if rows != 1 {
return gerror.New("订单状态已变化,请刷新后重试")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{
Module: "trade", Action: "trade_order.match", TargetType: "trade_order", TargetId: order.Id,
Before: order, After: data, ChangeJson: gjson.New(g.Map{"productId": product.Id, "quantity": in.Quantity, "totalAmount": totalAmount}),
Reason: "后台人工匹配商品",
})
})
}
func (s *sSysTradeOrder) Apply(ctx context.Context, in *sysin.TradeOrderApplyInp) (res *sysin.FinanceApprovalModel, err error) {
var order *entity.TradeOrder
if err = s.model(ctx).WherePri(in.Id).Scan(&order); err != nil {
return
}
if order == nil {
return nil, gerror.New("交易订单不存在")
}
if order.ApprovalRequestId > 0 {
return nil, gerror.New("该订单已有待执行处理申请")
}
switch in.Action {
case consts.TradeActionCancelMatch:
if order.OrderStatus != consts.TradeOrderMatching {
return nil, gerror.New("仅匹配中订单可以取消匹配")
}
case consts.TradeActionRematch:
if order.OrderStatus != consts.TradeOrderTimeout && order.OrderStatus != consts.TradeOrderFailed {
return nil, gerror.New("仅超时或失败订单可以重新匹配")
}
case consts.TradeActionUnfreeze:
if order.OrderStatus != consts.TradeOrderFrozen && order.FreezeStatus != consts.TradeFreezeFrozen {
return nil, gerror.New("订单当前未冻结")
}
}
payload := g.Map{"id": order.Id, "action": in.Action, "reasonCode": in.ReasonCode, "reasonText": in.ReasonText}
approval, err := service.SysApproval().Create(ctx, &sysin.ApprovalCreateInp{ApprovalType: "trade_order_action", TargetType: "trade_order", TargetId: order.Id, Action: in.Action, Title: "交易订单人工处理 " + order.OrderNo, PayloadJson: gjson.New(payload), RiskLevel: consts.ApprovalRiskHigh, RequiredApprovals: 1, Remark: in.ReasonText})
if err != nil {
return nil, err
}
if err = service.SysApproval().Submit(ctx, &sysin.ApprovalSubmitInp{Id: approval.Id}); err != nil {
return nil, err
}
data := g.Map{dao.TradeOrder.Columns().ApprovalRequestId: approval.Id, dao.TradeOrder.Columns().UpdatedBy: contexts.GetUserId(ctx)}
if in.Action == consts.TradeActionUnfreeze {
data[dao.TradeOrder.Columns().FreezeStatus] = consts.TradeFreezePending
}
if _, err = s.model(ctx).WherePri(order.Id).Data(data).Update(); err != nil {
return nil, gerror.Wrap(err, "登记订单处理申请失败")
}
return &sysin.FinanceApprovalModel{ApprovalId: approval.Id, ApprovalNo: approval.ApprovalNo}, nil
}
func (s *sSysTradeOrder) Execute(ctx context.Context, in *sysin.TradeOrderExecuteInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var approval *entity.ApprovalRequest
if err := service.SysApproval().Model(ctx).WherePri(in.ApprovalId).LockUpdate().Scan(&approval); err != nil {
return err
}
if approval == nil || approval.ApprovalType != "trade_order_action" {
return gerror.New("订单处理审批不存在")
}
if approval.ApprovalStatus != consts.ApprovalStatusApproved || approval.ExecutedAt != nil {
return gerror.New("审批尚未通过或已执行")
}
payload := approval.PayloadJson.Map()
orderId := gjson.New(payload).Get("id").Uint64()
action := gjson.New(payload).Get("action").String()
reason := gjson.New(payload).Get("reasonText").String()
var order *entity.TradeOrder
if err := s.model(ctx).WherePri(orderId).LockUpdate().Scan(&order); err != nil {
return err
}
if order == nil || order.ApprovalRequestId != approval.Id {
return gerror.New("订单处理申请已失效")
}
oc := dao.TradeOrder.Columns()
data := g.Map{oc.ApprovalRequestId: 0, oc.UpdatedBy: contexts.GetUserId(ctx)}
switch action {
case consts.TradeActionCancelMatch:
if order.OrderStatus != consts.TradeOrderMatching {
return gerror.New("订单状态已变化,不能取消匹配")
}
data[oc.OrderStatus] = consts.TradeOrderCancelled
case consts.TradeActionRematch:
if order.OrderStatus != consts.TradeOrderTimeout && order.OrderStatus != consts.TradeOrderFailed {
return gerror.New("订单状态已变化,不能重新匹配")
}
data[oc.OrderStatus], data[oc.MatchedAt] = consts.TradeOrderMatching, nil
case consts.TradeActionUnfreeze:
if order.FreezeStatus != consts.TradeFreezePending {
return gerror.New("订单解冻状态已变化")
}
data[oc.FreezeStatus], data[oc.OrderStatus], data[oc.UnfrozenAt] = consts.TradeFreezeNormal, consts.TradeOrderPending, gtime.Now()
default:
return gerror.New("订单处理动作不合法")
}
if _, err := s.model(ctx).WherePri(order.Id).Data(data).Update(); err != nil {
return gerror.Wrap(err, "执行订单处理失败")
}
ac := dao.ApprovalRequest.Columns()
if _, err := service.SysApproval().Model(ctx).WherePri(approval.Id).Data(g.Map{ac.ApprovalStatus: consts.ApprovalStatusExecuted, ac.ExecutedAt: gtime.Now(), ac.UpdatedBy: contexts.GetUserId(ctx), ac.Version: gdb.Raw(ac.Version + "+1")}).Update(); err != nil {
return err
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "trade", Action: "trade_order." + action, TargetType: "trade_order", TargetId: order.Id, Before: order, After: data, Reason: reason, ApprovalNo: approval.ApprovalNo})
})
}
@@ -0,0 +1,281 @@
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/encrypt"
"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/gtime"
"github.com/gogf/gf/v2/util/grand"
)
type sSysWithdrawalOrder struct{}
func NewSysWithdrawalOrder() *sSysWithdrawalOrder { return &sSysWithdrawalOrder{} }
func init() { service.RegisterSysWithdrawalOrder(NewSysWithdrawalOrder()) }
func (s *sSysWithdrawalOrder) model(ctx context.Context) *gdb.Model {
return handler.Model(dao.WithdrawalOrder.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysWithdrawalOrder) listModel(ctx context.Context) *gdb.Model {
wc, mc, ac := dao.WithdrawalOrder.Columns(), dao.AppMember.Columns(), dao.WalletAccount.Columns()
pc, sc := dao.AppMemberPaymentAccount.Columns(), dao.SysAttachment.Columns()
// account_snapshot_cipher is deliberately excluded: full account data never enters ordinary API responses.
return s.model(ctx).As("o").
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("o", wc.MemberId)).
LeftJoin(dao.WalletAccount.Table()+" w", col("w", ac.MemberId)+"="+col("o", wc.MemberId)+" AND "+col("w", ac.Currency)+"='CNY'").
LeftJoin(dao.AppMemberPaymentAccount.Table()+" pa", col("pa", pc.Id)+"="+col("o", wc.PaymentAccountId)).
LeftJoin(dao.SysAttachment.Table()+" sa", col("sa", sc.Id)+"="+col("pa", pc.QrAttachmentId)).
Fields(col("o", wc.Id), col("o", wc.WithdrawalNo), col("o", wc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", ac.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", ac.FrozenAmount)), col("o", wc.RequestAmount), col("o", wc.FeeAmount), col("o", wc.ActualAmount), col("o", wc.Currency), col("o", wc.AccountMask), col("o", wc.BankName), col("o", wc.HolderMask), fmt.Sprintf("COALESCE(%s,0) payment_account_type", col("pa", pc.AccountType)), fmt.Sprintf("COALESCE(%s,'') qr_code_url", col("sa", sc.FileUrl)), col("o", wc.RiskResult), col("o", wc.WithdrawalStatus), col("o", wc.ReviewReasonCode), col("o", wc.ReviewReason), col("o", wc.ApprovalRequestId), col("o", wc.ExternalNo), col("o", wc.FreezeLedgerId), col("o", wc.FinalLedgerId), col("o", wc.ReviewedAt), col("o", wc.ProcessedAt), col("o", wc.CompletedAt), col("o", wc.CreatedAt))
}
func (s *sSysWithdrawalOrder) List(ctx context.Context, in *sysin.WithdrawalOrderListInp) (list []*sysin.WithdrawalOrderListModel, total int, err error) {
wc, mc := dao.WithdrawalOrder.Columns(), dao.AppMember.Columns()
mod := s.listModel(ctx)
if in.WithdrawalNo != "" {
mod = mod.WhereLike(col("o", wc.WithdrawalNo), "%"+in.WithdrawalNo+"%")
}
if in.Username != "" {
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, e := encrypt.HashField(ctx, in.Mobile)
if e != nil {
return nil, 0, e
}
mod = mod.Where(col("m", mc.MobileHash), hash)
}
if in.WithdrawalStatus > 0 {
mod = mod.Where(col("o", wc.WithdrawalStatus), in.WithdrawalStatus)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(col("o", wc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1])
}
err = mod.Page(in.Page, in.PerPage).OrderDesc(col("o", wc.Id)).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取提现订单失败")
}
return
}
func (s *sSysWithdrawalOrder) View(ctx context.Context, in *sysin.WithdrawalOrderViewInp) (res *sysin.WithdrawalOrderListModel, err error) {
err = s.listModel(ctx).Where(col("o", dao.WithdrawalOrder.Columns().Id), in.Id).Scan(&res)
if err != nil {
return nil, gerror.Wrap(err, "获取提现详情失败")
}
if res == nil {
return nil, gerror.New("提现订单不存在")
}
return
}
func (s *sSysWithdrawalOrder) Review(ctx context.Context, in *sysin.WithdrawalReviewInp) (res *sysin.FinanceApprovalModel, err error) {
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var order *entity.WithdrawalOrder
if scanErr := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&order); scanErr != nil {
return scanErr
}
if order == nil {
return gerror.New("提现订单不存在")
}
if order.WithdrawalStatus != consts.WithdrawalPending || order.ApprovalRequestId > 0 {
return gerror.New("提现单已处理,请刷新列表")
}
if in.Decision == consts.FinanceDecisionApprove {
var member *entity.AppMember
if scanErr := handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(order.MemberId).Scan(&member); scanErr != nil {
return scanErr
}
if member == nil || member.MemberStatus != consts.AppMemberStatusNormal || member.RiskStatus == consts.AppMemberRiskBlocked {
return gerror.New("会员或风控状态不允许提现")
}
}
var ledgerId uint64
var handleErr error
status := consts.WithdrawalSuccess
if in.Decision == consts.FinanceDecisionApprove {
ledgerId, handleErr = s.consumeWithdrawal(ctx, order, in.ReasonText)
} else {
status = consts.WithdrawalRejected
ledgerId, handleErr = s.releaseWithdrawal(ctx, order, "提现审核驳回:"+in.ReasonText)
}
if handleErr != nil {
return handleErr
}
now := gtime.Now()
c := dao.WithdrawalOrder.Columns()
if _, updateErr := s.model(ctx).WherePri(order.Id).Data(g.Map{c.WithdrawalStatus: status, c.ReviewReasonCode: in.ReasonCode, c.ReviewReason: in.ReasonText, c.FinalLedgerId: ledgerId, c.ReviewedAt: now, c.ProcessedAt: now, c.CompletedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); updateErr != nil {
return updateErr
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "withdrawal", Action: "withdrawal.review", TargetType: "withdrawal_order", TargetId: order.Id, Before: order, After: g.Map{"decision": in.Decision, "status": status, "ledgerId": ledgerId}, Reason: in.ReasonText})
})
if err != nil {
return nil, err
}
return &sysin.FinanceApprovalModel{}, nil
}
func (s *sSysWithdrawalOrder) Execute(ctx context.Context, in *sysin.FinanceExecuteInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var approval *entity.ApprovalRequest
if err := service.SysApproval().Model(ctx).WherePri(in.ApprovalId).LockUpdate().Scan(&approval); err != nil {
return err
}
if approval == nil || approval.ApprovalType != "withdrawal_review" {
return gerror.New("提现审批不存在")
}
if approval.ApprovalStatus != consts.ApprovalStatusApproved || approval.ExecutedAt != nil {
return gerror.New("审批尚未通过或已执行")
}
var input sysin.WithdrawalReviewInp
if err := approval.PayloadJson.Scan(&input); err != nil {
return gerror.Wrap(err, "解析提现审批数据失败")
}
var order *entity.WithdrawalOrder
if err := s.model(ctx).WherePri(input.Id).LockUpdate().Scan(&order); err != nil {
return err
}
if order == nil || order.ApprovalRequestId != approval.Id || order.WithdrawalStatus != consts.WithdrawalReviewing {
return gerror.New("提现订单状态已变化")
}
c := dao.WithdrawalOrder.Columns()
now := gtime.Now()
finalLedgerId := uint64(0)
if input.Decision == consts.FinanceDecisionApprove {
if _, err := s.model(ctx).WherePri(order.Id).Data(g.Map{c.WithdrawalStatus: consts.WithdrawalProcessing, c.ReviewedAt: now, c.ProcessedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return err
}
} else {
id, err := s.releaseWithdrawal(ctx, order, "提现审核驳回:"+input.ReasonText)
if err != nil {
return err
}
finalLedgerId = id
if _, err = s.model(ctx).WherePri(order.Id).Data(g.Map{c.WithdrawalStatus: consts.WithdrawalRejected, c.FinalLedgerId: finalLedgerId, c.ReviewedAt: now, c.CompletedAt: now, c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return err
}
}
if err := recordFinanceReview(ctx, approval, "withdrawal", order.Id, input.Decision, input.ReasonCode, input.ReasonText, order); err != nil {
return err
}
if err := markApprovalExecuted(ctx, approval); err != nil {
return err
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "withdrawal", Action: "withdrawal.review.execute", TargetType: "withdrawal_order", TargetId: order.Id, Before: order, After: g.Map{"decision": input.Decision, "finalLedgerId": finalLedgerId}, Reason: input.ReasonText, ApprovalNo: approval.ApprovalNo})
})
}
func (s *sSysWithdrawalOrder) Complete(ctx context.Context, in *sysin.WithdrawalCompleteInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var order *entity.WithdrawalOrder
if err := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&order); err != nil {
return err
}
if order == nil {
return gerror.New("提现订单不存在")
}
if order.WithdrawalStatus != consts.WithdrawalProcessing {
return gerror.New("仅付款中的提现单可以登记结果")
}
var ledgerId uint64
var err error
status := consts.WithdrawalFailed
if in.Success {
ledgerId, err = s.consumeWithdrawal(ctx, order, in.ReasonText)
status = consts.WithdrawalSuccess
} else {
ledgerId, err = s.releaseWithdrawal(ctx, order, "付款失败退回:"+in.ReasonText)
}
if err != nil {
return err
}
c := dao.WithdrawalOrder.Columns()
if _, err = s.model(ctx).WherePri(order.Id).Data(g.Map{c.WithdrawalStatus: status, c.ExternalNo: in.ExternalNo, c.FinalLedgerId: ledgerId, c.CompletedAt: gtime.Now(), c.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return err
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "withdrawal", Action: "withdrawal.payment.complete", TargetType: "withdrawal_order", TargetId: order.Id, Before: order, After: g.Map{"status": status, "externalNo": in.ExternalNo, "ledgerId": ledgerId}, Reason: in.ReasonText})
})
}
func (s *sSysWithdrawalOrder) wallet(ctx context.Context, order *entity.WithdrawalOrder) (*entity.WalletAccount, error) {
c := dao.WalletAccount.Columns()
var account *entity.WalletAccount
if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(c.MemberId, order.MemberId).Where(c.Currency, order.Currency).LockUpdate().Scan(&account); err != nil {
return nil, err
}
if account == nil {
return nil, gerror.New("会员钱包不存在")
}
if account.FrozenAmount < order.RequestAmount {
return nil, gerror.New("提现冻结金额不足,不能完成处理")
}
return account, nil
}
func (s *sSysWithdrawalOrder) consumeWithdrawal(ctx context.Context, order *entity.WithdrawalOrder, remark string) (uint64, error) {
account, err := s.wallet(ctx, order)
if err != nil {
return 0, err
}
ledgerId, err := insertWithdrawalLedger(ctx, account, order, "withdraw", "frozen", consts.WalletDirectionExpense, order.RequestAmount, account.FrozenAmount, account.FrozenAmount-order.RequestAmount, fmt.Sprintf("withdraw:%d:consume", order.Id), remark)
if err != nil {
return 0, err
}
c := dao.WalletAccount.Columns()
result, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(c.Version, account.Version).Data(g.Map{c.FrozenAmount: account.FrozenAmount - order.RequestAmount, c.TotalExpense: gdb.Raw(c.TotalExpense + "+" + fmt.Sprint(order.RequestAmount)), c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return 0, err
}
rows, _ := result.RowsAffected()
if rows != 1 {
return 0, gerror.New("钱包余额已变化,请重试")
}
return ledgerId, nil
}
func (s *sSysWithdrawalOrder) releaseWithdrawal(ctx context.Context, order *entity.WithdrawalOrder, remark string) (uint64, error) {
account, err := s.wallet(ctx, order)
if err != nil {
return 0, err
}
_, err = insertWithdrawalLedger(ctx, account, order, "withdraw_release", "frozen", consts.WalletDirectionExpense, order.RequestAmount, account.FrozenAmount, account.FrozenAmount-order.RequestAmount, fmt.Sprintf("withdraw:%d:release:frozen", order.Id), remark)
if err != nil {
return 0, err
}
ledgerId, err := insertWithdrawalLedger(ctx, account, order, "withdraw_release", "available", consts.WalletDirectionIncome, order.RequestAmount, account.AvailableAmount, account.AvailableAmount+order.RequestAmount, fmt.Sprintf("withdraw:%d:release:available", order.Id), remark)
if err != nil {
return 0, err
}
c := dao.WalletAccount.Columns()
result, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(c.Version, account.Version).Data(g.Map{c.FrozenAmount: account.FrozenAmount - order.RequestAmount, c.AvailableAmount: account.AvailableAmount + order.RequestAmount, c.Version: gdb.Raw(c.Version + "+1")}).Update()
if err != nil {
return 0, err
}
rows, _ := result.RowsAffected()
if rows != 1 {
return 0, gerror.New("钱包余额已变化,请重试")
}
return ledgerId, nil
}
func insertWithdrawalLedger(ctx context.Context, account *entity.WalletAccount, order *entity.WithdrawalOrder, bizType, entryType string, direction int, amount, before, after int64, idempotencyKey, remark string) (uint64, error) {
c := dao.WalletLedger.Columns()
result, err := dao.WalletLedger.Ctx(ctx).Fields(c.LedgerNo, c.AccountId, c.MemberId, c.BizType, c.BizNo, c.EntryType, c.Direction, c.Amount, c.BeforeAmount, c.AfterAmount, c.Currency, c.IdempotencyKey, c.Remark, c.OperatorId, c.OccurredAt).Hook(hook.SaveTenant).Data(g.Map{c.LedgerNo: "WL" + gtime.Now().Format("YmdHis") + grand.S(7), c.AccountId: account.Id, c.MemberId: order.MemberId, c.BizType: bizType, c.BizNo: order.WithdrawalNo, c.EntryType: entryType, c.Direction: direction, c.Amount: amount, c.BeforeAmount: before, c.AfterAmount: after, c.Currency: account.Currency, c.IdempotencyKey: idempotencyKey, c.Remark: remark, c.OperatorId: contexts.GetUserId(ctx), c.OccurredAt: gtime.Now()}).Insert()
if err != nil {
return 0, gerror.Wrap(err, "写入提现台账失败")
}
id, _ := result.LastInsertId()
return uint64(id), nil
}