feat: add member URL pool and login tracking

This commit is contained in:
richjohnrambo
2026-10-15 10:41:29 +08:00
parent ca0d115e36
commit f8b5e4270b
46 changed files with 3733 additions and 66 deletions
+4 -4
View File
@@ -3,12 +3,12 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package consts
// 消息队列
const (
QueueLogTopic = `request_log` // 访问日志
QueueLoginLogTopic = `login_log` // 登录日志
QueueServeLogTopic = `serve_log` // 服务日志
QueueLogTopic = `request_log` // 访问日志
QueueLoginLogTopic = `login_log` // 登录日志
QueueAppMemberLoginLogTopic = `app_member_login_log` // C端会员登录日志
QueueServeLogTopic = `serve_log` // 服务日志
)
+40
View File
@@ -0,0 +1,40 @@
package consts
import (
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
const (
UrlPoolStatusEnabled = 1
UrlPoolStatusInvalid = 2
AppMemberLoginTypePassword = 1
AppMemberLoginTypeSms = 2
AppMemberDomainBoundEnabled = 1
AppMemberDomainUnbound = 2
AppMemberDomainBoundInvalid = 3
)
var (
UrlPoolStatusOptions = []*model.Option{
dict.GenSuccessOption(UrlPoolStatusEnabled, "启用"),
dict.GenWarningOption(UrlPoolStatusInvalid, "失效"),
}
AppMemberLoginTypeOptions = []*model.Option{
dict.GenPrimaryOption(AppMemberLoginTypePassword, "账号密码"),
dict.GenInfoOption(AppMemberLoginTypeSms, "手机验证码"),
}
AppMemberDomainStatusOptions = []*model.Option{
dict.GenSuccessOption(AppMemberDomainBoundEnabled, "已绑定"),
dict.GenWarningOption(AppMemberDomainUnbound, "待绑定"),
dict.GenErrorOption(AppMemberDomainBoundInvalid, "域名已失效"),
}
)
func init() {
dict.RegisterEnums("urlPoolStatus", "URL地址状态", UrlPoolStatusOptions)
dict.RegisterEnums("appMemberLoginTypeOptions", "会员登录方式", AppMemberLoginTypeOptions)
dict.RegisterEnums("appMemberDomainStatus", "会员域名绑定状态", AppMemberDomainStatusOptions)
}
@@ -0,0 +1,39 @@
package sys
import (
"context"
"hotgo/api/admin/appmemberloginlog"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var AppMemberLoginLog = cAppMemberLoginLog{}
type cAppMemberLoginLog struct{}
func (c *cAppMemberLoginLog) List(ctx context.Context, req *appmemberloginlog.ListReq) (res *appmemberloginlog.ListRes, err error) {
list, total, err := service.SysAppMemberLoginLog().List(ctx, &req.AppMemberLoginLogListInp)
if err != nil {
return nil, err
}
if list == nil {
list = []*sysin.AppMemberLoginLogListModel{}
}
res = new(appmemberloginlog.ListRes)
res.List = list
res.PageRes.Pack(req, total)
return
}
func (c *cAppMemberLoginLog) View(ctx context.Context, req *appmemberloginlog.ViewReq) (*appmemberloginlog.ViewRes, error) {
data, err := service.SysAppMemberLoginLog().View(ctx, &req.AppMemberLoginLogViewInp)
if err != nil {
return nil, err
}
return &appmemberloginlog.ViewRes{AppMemberLoginLogViewModel: data}, nil
}
func (c *cAppMemberLoginLog) Export(ctx context.Context, req *appmemberloginlog.ExportReq) (*appmemberloginlog.ExportRes, error) {
return &appmemberloginlog.ExportRes{}, service.SysAppMemberLoginLog().Export(ctx, &req.AppMemberLoginLogListInp)
}
@@ -0,0 +1,91 @@
package sys
import (
"context"
"strings"
"hotgo/api/admin/urlpool"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
)
var UrlPool = cUrlPool{}
type cUrlPool struct{}
func (c *cUrlPool) List(ctx context.Context, req *urlpool.ListReq) (res *urlpool.ListRes, err error) {
list, total, err := service.SysUrlPool().List(ctx, &req.UrlPoolListInp)
if err != nil {
return nil, err
}
if list == nil {
list = []*sysin.UrlPoolListModel{}
}
res = new(urlpool.ListRes)
res.List = list
res.PageRes.Pack(req, total)
return
}
func (c *cUrlPool) View(ctx context.Context, req *urlpool.ViewReq) (*urlpool.ViewRes, error) {
data, err := service.SysUrlPool().View(ctx, &req.UrlPoolViewInp)
if err != nil {
return nil, err
}
return &urlpool.ViewRes{UrlPoolViewModel: data}, nil
}
func (c *cUrlPool) Edit(ctx context.Context, req *urlpool.EditReq) (*urlpool.EditRes, error) {
return &urlpool.EditRes{}, service.SysUrlPool().Edit(ctx, &req.UrlPoolEditInp)
}
func (c *cUrlPool) Status(ctx context.Context, req *urlpool.StatusReq) (*urlpool.StatusRes, error) {
return &urlpool.StatusRes{}, service.SysUrlPool().Status(ctx, &req.UrlPoolStatusInp)
}
func (c *cUrlPool) Import(ctx context.Context, req *urlpool.ImportReq) (*urlpool.ImportRes, error) {
if req.File == nil {
return nil, gerror.New("请选择Excel文件")
}
filename := strings.ToLower(strings.TrimSpace(req.File.Filename))
if !strings.HasSuffix(filename, ".xlsx") && !strings.HasSuffix(filename, ".xlsm") {
return nil, gerror.New("仅支持.xlsx或.xlsm格式的Excel文件")
}
if req.File.Size > 10*1024*1024 {
return nil, gerror.New("Excel文件不能超过10MB")
}
file, err := req.File.Open()
if err != nil {
return nil, gerror.Wrap(err, "读取Excel文件失败")
}
defer file.Close()
result, err := service.SysUrlPool().Import(ctx, file)
if err != nil {
return nil, err
}
return &urlpool.ImportRes{UrlPoolImportModel: result}, nil
}
func (c *cUrlPool) MemberList(ctx context.Context, req *urlpool.MemberListReq) (res *urlpool.MemberListRes, err error) {
list, total, err := service.SysUrlPool().MemberList(ctx, &req.UrlPoolMemberListInp)
if err != nil {
return nil, err
}
if list == nil {
list = []*sysin.UrlPoolMemberListModel{}
}
res = new(urlpool.MemberListRes)
res.List = list
res.PageRes.Pack(req, total)
return
}
func (c *cUrlPool) Summary(ctx context.Context, req *urlpool.SummaryReq) (*urlpool.SummaryRes, error) {
data, err := service.SysUrlPool().Summary(ctx, &req.UrlPoolSummaryInp)
if err != nil {
return nil, err
}
return &urlpool.SummaryRes{UrlPoolSummaryModel: data}, nil
}
@@ -0,0 +1,32 @@
package member
import (
"context"
v1 "hotgo/api/api/member/v1"
"hotgo/internal/service"
)
func (c *ControllerV1) Register(ctx context.Context, req *v1.RegisterReq) (*v1.RegisterRes, error) {
data, err := service.SysAppMemberAuth().Register(ctx, &req.AppMemberRegisterInp)
if err != nil {
return nil, err
}
return &v1.RegisterRes{AppMemberRegisterModel: data}, nil
}
func (c *ControllerV1) AccountLogin(ctx context.Context, req *v1.AccountLoginReq) (*v1.AccountLoginRes, error) {
data, err := service.SysAppMemberAuth().AccountLogin(ctx, &req.AppMemberAccountLoginInp)
if err != nil {
return nil, err
}
return &v1.AccountLoginRes{AppMemberLoginModel: data}, nil
}
func (c *ControllerV1) MobileLogin(ctx context.Context, req *v1.MobileLoginReq) (*v1.MobileLoginRes, error) {
data, err := service.SysAppMemberAuth().MobileLogin(ctx, &req.AppMemberMobileLoginInp)
if err != nil {
return nil, err
}
return &v1.MobileLoginRes{AppMemberLoginModel: data}, nil
}
@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"hotgo/internal/dao/internal"
)
// appMemberLoginLogDao is the data access object for the table hg_app_member_login_log.
// You can define custom methods on it to extend its functionality as needed.
type appMemberLoginLogDao struct {
*internal.AppMemberLoginLogDao
}
var (
// AppMemberLoginLog is a globally accessible object for table hg_app_member_login_log operations.
AppMemberLoginLog = appMemberLoginLogDao{internal.NewAppMemberLoginLogDao()}
)
// Add your custom methods and functionality below.
@@ -0,0 +1,109 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// AppMemberLoginLogDao is the data access object for the table hg_app_member_login_log.
type AppMemberLoginLogDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns AppMemberLoginLogColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// AppMemberLoginLogColumns defines and stores column names for the table hg_app_member_login_log.
type AppMemberLoginLogColumns struct {
Id string // 日志ID
TenantId string // 租户ID
ReqId string // 请求ID
MemberId string // 会员ID,未识别会员时为0
Username string // 用户名快照
MobileMask string // 手机号脱敏值
LoginType string // 登录方式:1账号密码,2手机验证码
Status string // 登录状态:1成功,2失败
ErrMsg string // 失败原因
LoginAt string // 登录时间
LoginIpCipher string // 登录IP密文
LoginIpMask string // 登录IP脱敏值
LoginRegion string // 登录地区
LoginDomain string // 登录页面标准化域名
UserAgent string // 浏览器UA
CreatedAt string // 创建时间
}
// appMemberLoginLogColumns holds the columns for the table hg_app_member_login_log.
var appMemberLoginLogColumns = AppMemberLoginLogColumns{
Id: "id",
TenantId: "tenant_id",
ReqId: "req_id",
MemberId: "member_id",
Username: "username",
MobileMask: "mobile_mask",
LoginType: "login_type",
Status: "status",
ErrMsg: "err_msg",
LoginAt: "login_at",
LoginIpCipher: "login_ip_cipher",
LoginIpMask: "login_ip_mask",
LoginRegion: "login_region",
LoginDomain: "login_domain",
UserAgent: "user_agent",
CreatedAt: "created_at",
}
// NewAppMemberLoginLogDao creates and returns a new DAO object for table data access.
func NewAppMemberLoginLogDao(handlers ...gdb.ModelHandler) *AppMemberLoginLogDao {
return &AppMemberLoginLogDao{
group: "default",
table: "hg_app_member_login_log",
columns: appMemberLoginLogColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *AppMemberLoginLogDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *AppMemberLoginLogDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *AppMemberLoginLogDao) Columns() AppMemberLoginLogColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *AppMemberLoginLogDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *AppMemberLoginLogDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *AppMemberLoginLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -0,0 +1,95 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// MemberUrlBindingDao is the data access object for the table hg_member_url_binding.
type MemberUrlBindingDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns MemberUrlBindingColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// MemberUrlBindingColumns defines and stores column names for the table hg_member_url_binding.
type MemberUrlBindingColumns struct {
Id string // 主键
TenantId string // 租户ID
MemberId string // 会员ID
UrlPoolId string // URL地址池ID
DomainSnapshot string // 绑定时域名快照
OriginSnapshot string // 绑定时Origin快照
BoundAt string // 绑定时间
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
}
// memberUrlBindingColumns holds the columns for the table hg_member_url_binding.
var memberUrlBindingColumns = MemberUrlBindingColumns{
Id: "id",
TenantId: "tenant_id",
MemberId: "member_id",
UrlPoolId: "url_pool_id",
DomainSnapshot: "domain_snapshot",
OriginSnapshot: "origin_snapshot",
BoundAt: "bound_at",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
}
// NewMemberUrlBindingDao creates and returns a new DAO object for table data access.
func NewMemberUrlBindingDao(handlers ...gdb.ModelHandler) *MemberUrlBindingDao {
return &MemberUrlBindingDao{
group: "default",
table: "hg_member_url_binding",
columns: memberUrlBindingColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *MemberUrlBindingDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *MemberUrlBindingDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *MemberUrlBindingDao) Columns() MemberUrlBindingColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *MemberUrlBindingDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *MemberUrlBindingDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *MemberUrlBindingDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
+111
View File
@@ -0,0 +1,111 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// UrlPoolDao is the data access object for the table hg_url_pool.
type UrlPoolDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns UrlPoolColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// UrlPoolColumns defines and stores column names for the table hg_url_pool.
type UrlPoolColumns struct {
Id string // 主键
TenantId string // 租户ID
Name string // 地址名称
Scheme string // 协议:http或https
Domain string // 标准化域名,可含非默认端口
DomainHash string // 标准化域名SHA256
BaseUrl string // 注册入口基础地址
Status string // 状态:1启用,2失效
BindCount string // 累计成功绑定次数
LastBoundAt string // 最后绑定时间
Remark string // 备注
CreatedBy string // 创建者ID
UpdatedBy string // 更新者ID
DeletedBy string // 删除者ID
CreatedAt string // 创建时间
UpdatedAt string // 更新时间
DeletedAt string // 删除时间
}
// urlPoolColumns holds the columns for the table hg_url_pool.
var urlPoolColumns = UrlPoolColumns{
Id: "id",
TenantId: "tenant_id",
Name: "name",
Scheme: "scheme",
Domain: "domain",
DomainHash: "domain_hash",
BaseUrl: "base_url",
Status: "status",
BindCount: "bind_count",
LastBoundAt: "last_bound_at",
Remark: "remark",
CreatedBy: "created_by",
UpdatedBy: "updated_by",
DeletedBy: "deleted_by",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// NewUrlPoolDao creates and returns a new DAO object for table data access.
func NewUrlPoolDao(handlers ...gdb.ModelHandler) *UrlPoolDao {
return &UrlPoolDao{
group: "default",
table: "hg_url_pool",
columns: urlPoolColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *UrlPoolDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *UrlPoolDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *UrlPoolDao) Columns() UrlPoolColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *UrlPoolDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *UrlPoolDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *UrlPoolDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
+22
View File
@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"hotgo/internal/dao/internal"
)
// memberUrlBindingDao is the data access object for the table hg_member_url_binding.
// You can define custom methods on it to extend its functionality as needed.
type memberUrlBindingDao struct {
*internal.MemberUrlBindingDao
}
var (
// MemberUrlBinding is a globally accessible object for table hg_member_url_binding operations.
MemberUrlBinding = memberUrlBindingDao{internal.NewMemberUrlBindingDao()}
)
// Add your custom methods and functionality below.
+22
View File
@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"hotgo/internal/dao/internal"
)
// urlPoolDao is the data access object for the table hg_url_pool.
// You can define custom methods on it to extend its functionality as needed.
type urlPoolDao struct {
*internal.UrlPoolDao
}
var (
// UrlPool is a globally accessible object for table hg_url_pool operations.
UrlPool = urlPoolDao{internal.NewUrlPoolDao()}
)
// Add your custom methods and functionality below.
+22 -1
View File
@@ -55,10 +55,13 @@ 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()
bc, uc := dao.MemberUrlBinding.Columns(), dao.UrlPool.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)).
LeftJoin(dao.MemberUrlBinding.Table()+" ub", col("ub", bc.MemberId)+"="+col("m", mc.Id)).
LeftJoin(dao.UrlPool.Table()+" up", col("up", uc.Id)+"="+col("ub", bc.UrlPoolId)).
Fields(
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.Mobile),
col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)),
@@ -67,6 +70,9 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l
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),
fmt.Sprintf("COALESCE(%s,0) url_pool_id", col("ub", bc.UrlPoolId)),
fmt.Sprintf("COALESCE(%s,'') register_domain", col("ub", bc.DomainSnapshot)), col("ub", bc.BoundAt)+" domain_bound_at",
fmt.Sprintf("CASE WHEN %s IS NULL THEN %d WHEN %s=%d THEN %d ELSE %d END domain_status", col("ub", bc.Id), consts.AppMemberDomainUnbound, col("up", uc.Status), consts.UrlPoolStatusEnabled, consts.AppMemberDomainBoundEnabled, consts.AppMemberDomainBoundInvalid),
)
if in.Username != "" {
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
@@ -85,6 +91,16 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l
} else if in.MemberCategory == consts.AppMemberCategoryFake {
mod = mod.Where(col("m", mc.IsTestAccount), 1)
}
if in.RegisterDomain != "" {
mod = mod.WhereLike(col("ub", bc.DomainSnapshot), "%"+strings.ToLower(strings.TrimSpace(in.RegisterDomain))+"%")
}
if in.DomainStatus == consts.AppMemberDomainBoundEnabled {
mod = mod.WhereNotNull(col("ub", bc.Id)).Where(col("up", uc.Status), consts.UrlPoolStatusEnabled)
} else if in.DomainStatus == consts.AppMemberDomainUnbound {
mod = mod.WhereNull(col("ub", bc.Id))
} else if in.DomainStatus == consts.AppMemberDomainBoundInvalid {
mod = mod.WhereNotNull(col("ub", bc.Id)).Where(col("up", uc.Status), consts.UrlPoolStatusInvalid)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(col("m", mc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1])
}
@@ -97,9 +113,11 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l
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()
bc, uc := dao.MemberUrlBinding.Columns(), dao.UrlPool.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(
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)).LeftJoin(dao.MemberUrlBinding.Table()+" ub", col("ub", bc.MemberId)+"="+col("m", mc.Id)).LeftJoin(dao.UrlPool.Table()+" up", col("up", uc.Id)+"="+col("ub", bc.UrlPoolId)).Fields(
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.Mobile), 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),
fmt.Sprintf("COALESCE(%s,0) url_pool_id", col("ub", bc.UrlPoolId)), fmt.Sprintf("COALESCE(%s,'') register_domain", col("ub", bc.DomainSnapshot)), col("ub", bc.BoundAt)+" domain_bound_at", fmt.Sprintf("CASE WHEN %s IS NULL THEN %d WHEN %s=%d THEN %d ELSE %d END domain_status", col("ub", bc.Id), consts.AppMemberDomainUnbound, col("up", uc.Status), consts.UrlPoolStatusEnabled, consts.AppMemberDomainBoundEnabled, consts.AppMemberDomainBoundInvalid),
).Where(col("m", mc.Id), in.Id).Scan(res)
if err != nil {
return nil, gerror.Wrap(err, "获取会员详情失败")
@@ -115,6 +133,9 @@ func (s *sSysAppMember) View(ctx context.Context, in *sysin.AppMemberViewInp) (r
}
func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) error {
if in.Id == 0 {
return gerror.New("后台不支持创建会员,请由会员在C端完成注册")
}
isTestAccount := 2
if in.MemberCategory == consts.AppMemberCategoryFake {
isTestAccount = 1
@@ -0,0 +1,206 @@
package sys
import (
"context"
"fmt"
"strings"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/location"
"hotgo/internal/library/token"
"hotgo/internal/model"
"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/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/grand"
"golang.org/x/crypto/bcrypt"
)
type sSysAppMemberAuth struct{}
func NewSysAppMemberAuth() *sSysAppMemberAuth { return &sSysAppMemberAuth{} }
func init() { service.RegisterSysAppMemberAuth(NewSysAppMemberAuth()) }
func (s *sSysAppMemberAuth) memberModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysAppMemberAuth) Register(ctx context.Context, in *sysin.AppMemberRegisterInp) (res *sysin.AppMemberRegisterModel, err error) {
mc, wc, lc := dao.AppMember.Columns(), dao.WalletAccount.Columns(), dao.AppMemberLevel.Columns()
res = new(sysin.AppMemberRegisterModel)
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
count, e := s.memberModel(ctx).Where(mc.Username, in.Username).Count()
if e != nil {
return gerror.Wrap(e, "检查用户名失败")
}
if count > 0 {
return gerror.New("用户名已存在")
}
count, e = s.memberModel(ctx).Where(mc.Mobile, in.Mobile).Count()
if e != nil {
return gerror.Wrap(e, "检查手机号失败")
}
if count > 0 {
return gerror.New("手机号已存在")
}
parentId, level, tree := uint64(0), 1, ""
if in.InviteCode != "" {
var parent *entity.AppMember
if e = s.memberModel(ctx).Where(mc.InviteCode, in.InviteCode).Scan(&parent); e != nil {
return gerror.Wrap(e, "查询邀请人失败")
}
if parent == nil {
return gerror.New("邀请码不存在")
}
parentId = parent.Id
level = parent.Level + 1
tree = parent.Tree + "tr_" + fmt.Sprint(parent.Id) + " "
}
levelId := uint64(0)
var firstLevel *entity.AppMemberLevel
if e = handler.Model(dao.AppMemberLevel.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(lc.TenantId, 0).Where(lc.Status, consts.StatusEnabled).OrderAsc(lc.Sort).OrderAsc(lc.Id).Scan(&firstLevel); e != nil {
return gerror.Wrap(e, "查询默认会员等级失败")
}
if firstLevel != nil {
levelId = firstLevel.Id
}
hashed, e := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if e != nil {
return gerror.Wrap(e, "生成密码摘要失败")
}
memberNo := "MB" + gtime.Now().Format("YmdHis") + grand.S(5)
memberData := g.Map{
mc.TenantId: 0,
mc.MemberNo: memberNo, mc.Username: in.Username, mc.Mobile: in.Mobile,
mc.PasswordHash: string(hashed), mc.LevelId: levelId, mc.ParentId: parentId,
mc.Level: level, mc.Tree: tree, mc.InviteCode: strings.ToUpper(grand.S(8)),
mc.MemberStatus: consts.AppMemberStatusNormal, mc.RiskStatus: consts.AppMemberRiskNormal,
mc.AutoDispatch: consts.AppMemberDispatchManual, mc.CreditScore: 100, mc.IsTestAccount: 2,
}
result, e := dao.AppMember.Ctx(ctx).Fields(
mc.TenantId, mc.MemberNo, mc.Username, mc.Mobile, mc.PasswordHash, mc.LevelId, mc.ParentId, mc.Level,
mc.Tree, mc.InviteCode, mc.MemberStatus, mc.RiskStatus, mc.AutoDispatch, mc.CreditScore, mc.IsTestAccount,
).Data(memberData).Insert()
if e != nil {
return gerror.Wrap(e, "创建会员失败")
}
id, e := result.LastInsertId()
if e != nil {
return gerror.Wrap(e, "读取会员ID失败")
}
res.MemberId = uint64(id)
res.MemberNo = memberNo
if _, e = dao.WalletAccount.Ctx(ctx).Fields(wc.TenantId, wc.MemberId, wc.Currency).Data(g.Map{wc.TenantId: 0, wc.MemberId: res.MemberId, wc.Currency: "CNY"}).Insert(); e != nil {
return gerror.Wrap(e, "初始化会员钱包失败")
}
r := ghttp.RequestFromCtx(ctx)
origin, host := in.RegisterOrigin, in.RegisterHost
if r != nil {
if r.Header.Get("Origin") != "" {
origin = r.Header.Get("Origin")
}
if host == "" {
host = r.Host
}
}
res.Bound, res.Domain, e = service.SysUrlPool().BindOnRegister(ctx, res.MemberId, origin, host)
return e
})
return
}
func (s *sSysAppMemberAuth) AccountLogin(ctx context.Context, in *sysin.AppMemberAccountLoginInp) (res *sysin.AppMemberLoginModel, err error) {
var member *entity.AppMember
defer func() {
service.SysAppMemberLoginLog().Push(ctx, &sysin.AppMemberLoginLogPushInp{Member: member, LoginType: consts.AppMemberLoginTypePassword, Account: in.Account, Err: err})
}()
mc := dao.AppMember.Columns()
if err = s.memberModel(ctx).Where(fmt.Sprintf("(%s=? OR %s=?)", mc.Username, mc.Mobile), in.Account, in.Account).Scan(&member); err != nil {
err = gerror.Wrap(err, "登录失败,请稍后重试")
return
}
if member == nil || bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte(in.Password)) != nil {
err = gerror.New("账号或密码错误")
return
}
return s.finishLogin(ctx, member)
}
func (s *sSysAppMemberAuth) MobileLogin(ctx context.Context, in *sysin.AppMemberMobileLoginInp) (res *sysin.AppMemberLoginModel, err error) {
var member *entity.AppMember
defer func() {
service.SysAppMemberLoginLog().Push(ctx, &sysin.AppMemberLoginLogPushInp{Member: member, LoginType: consts.AppMemberLoginTypeSms, Account: in.Mobile, Err: err})
}()
mc := dao.AppMember.Columns()
if err = s.memberModel(ctx).Where(mc.Mobile, in.Mobile).Scan(&member); err != nil {
err = gerror.Wrap(err, "登录失败,请稍后重试")
return
}
if member == nil {
err = gerror.New("会员不存在")
return
}
if err = service.SysSmsLog().VerifyCode(ctx, &sysin.VerifyCodeInp{Event: consts.SmsTemplateLogin, Mobile: in.Mobile, Code: in.Code}); err != nil {
err = gerror.New("验证码错误或已过期")
return
}
return s.finishLogin(ctx, member)
}
func (s *sSysAppMemberAuth) finishLogin(ctx context.Context, member *entity.AppMember) (res *sysin.AppMemberLoginModel, err error) {
if member.MemberStatus != consts.AppMemberStatusNormal {
return nil, gerror.New("会员已禁用")
}
if member.RiskStatus == consts.AppMemberRiskBlocked {
return nil, gerror.New("风控限制登录")
}
if err = s.updateLastLogin(ctx, member); err != nil {
return nil, err
}
now := gtime.Now()
identity := &model.Identity{Id: int64(member.Id), Username: member.Username, App: consts.AppApi, LoginAt: now}
loginToken, expires, err := token.Login(ctx, identity)
if err != nil {
return nil, gerror.Wrap(err, "登录失败,请稍后重试")
}
res = &sysin.AppMemberLoginModel{Id: member.Id, MemberNo: member.MemberNo, Username: member.Username, Mobile: encrypt.MaskMobile(member.Mobile), Token: loginToken, Expires: expires}
return
}
func (s *sSysAppMemberAuth) updateLastLogin(ctx context.Context, member *entity.AppMember) error {
r := ghttp.RequestFromCtx(ctx)
if r == nil {
return gerror.New("登录失败,请稍后重试")
}
clientIP := location.GetClientIp(r)
cipher, err := encrypt.EncryptField(ctx, clientIP)
if err != nil {
return gerror.Wrap(err, "登录失败,请稍后重试")
}
region := ""
if ipData, locationErr := location.GetLocation(ctx, clientIP); locationErr == nil && ipData != nil {
region = formatLoginRegion(ipData)
} else if locationErr != nil {
g.Log().Warningf(ctx, "解析会员登录地区失败 memberId:%d ip:%s err:%+v", member.Id, maskIP(clientIP), locationErr)
}
mc := dao.AppMember.Columns()
_, err = s.memberModel(ctx).Fields(mc.LastLoginAt, mc.LastLoginIpCipher, mc.LastLoginIpMask, mc.LastLoginRegion).WherePri(member.Id).Data(g.Map{
mc.LastLoginAt: gtime.Now(), mc.LastLoginIpCipher: cipher, mc.LastLoginIpMask: maskIP(clientIP), mc.LastLoginRegion: region,
}).Update()
if err != nil {
return gerror.Wrap(err, "登录失败,请稍后重试")
}
return nil
}
@@ -0,0 +1,233 @@
package sys
import (
"context"
"fmt"
"net"
"strings"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/location"
"hotgo/internal/library/queue"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/convert"
"hotgo/utility/encrypt"
"hotgo/utility/excel"
"hotgo/utility/useragent"
"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/net/ghttp"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysAppMemberLoginLog struct{}
func NewSysAppMemberLoginLog() *sSysAppMemberLoginLog { return &sSysAppMemberLoginLog{} }
func init() { service.RegisterSysAppMemberLoginLog(NewSysAppMemberLoginLog()) }
func (s *sSysAppMemberLoginLog) Model(ctx context.Context) *gdb.Model {
return handler.Model(dao.AppMemberLoginLog.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysAppMemberLoginLog) List(ctx context.Context, in *sysin.AppMemberLoginLogListInp) (list []*sysin.AppMemberLoginLogListModel, totalCount int, err error) {
lc, mc := dao.AppMemberLoginLog.Columns(), dao.AppMember.Columns()
mod := s.Model(ctx).As("l").LeftJoin(dao.AppMember.Table()+" m", "m."+mc.Id+"=l."+lc.MemberId)
if in.Keyword != "" {
keyword := strings.TrimSpace(in.Keyword)
like := "%" + keyword + "%"
mod = mod.Where(fmt.Sprintf("(m.%s LIKE ? OR l.%s LIKE ? OR l.%s LIKE ? OR l.%s LIKE ?)", mc.MemberNo, lc.Username, lc.MobileMask, lc.MemberId), like, like, like, like)
}
if in.LoginType > 0 {
mod = mod.Where("l."+lc.LoginType, in.LoginType)
}
if in.Status > 0 {
mod = mod.Where("l."+lc.Status, in.Status)
}
if in.LoginDomain != "" {
mod = mod.WhereLike("l."+lc.LoginDomain, "%"+strings.ToLower(strings.TrimSpace(in.LoginDomain))+"%")
}
if in.LoginIp != "" {
mod = mod.WhereLike("l."+lc.LoginIpMask, "%"+strings.TrimSpace(in.LoginIp)+"%")
}
if len(in.LoginAt) == 2 {
mod = mod.WhereBetween("l."+lc.LoginAt, in.LoginAt[0], in.LoginAt[1])
}
totalCount, err = mod.Clone().Count()
if err != nil || totalCount == 0 {
return
}
mod = mod.Fields(
"l."+lc.Id, "l."+lc.ReqId, "l."+lc.MemberId,
fmt.Sprintf("COALESCE(m.%s,'') member_no", mc.MemberNo),
"l."+lc.Username, "l."+lc.MobileMask, "l."+lc.LoginType, "l."+lc.Status,
"l."+lc.ErrMsg, "l."+lc.LoginAt, "l."+lc.LoginIpMask, "l."+lc.LoginRegion,
"l."+lc.LoginDomain, "l."+lc.UserAgent,
)
err = mod.Page(in.Page, in.PerPage).OrderDesc("l." + lc.LoginAt).OrderDesc("l." + lc.Id).Scan(&list)
if err != nil {
return nil, 0, gerror.Wrap(err, "查询会员登录日志失败")
}
for _, row := range list {
row.Os = useragent.GetOs(row.UserAgent)
row.Browser = useragent.GetBrowser(row.UserAgent)
}
return
}
func (s *sSysAppMemberLoginLog) View(ctx context.Context, in *sysin.AppMemberLoginLogViewInp) (res *sysin.AppMemberLoginLogViewModel, err error) {
lc, mc := dao.AppMemberLoginLog.Columns(), dao.AppMember.Columns()
res = new(sysin.AppMemberLoginLogViewModel)
err = s.Model(ctx).As("l").LeftJoin(dao.AppMember.Table()+" m", "m."+mc.Id+"=l."+lc.MemberId).Fields(
"l."+lc.Id, "l."+lc.ReqId, "l."+lc.MemberId,
fmt.Sprintf("COALESCE(m.%s,'') member_no", mc.MemberNo),
"l."+lc.Username, "l."+lc.MobileMask, "l."+lc.LoginType, "l."+lc.Status,
"l."+lc.ErrMsg, "l."+lc.LoginAt, "l."+lc.LoginIpMask, "l."+lc.LoginRegion,
"l."+lc.LoginDomain, "l."+lc.UserAgent,
).Where("l."+lc.Id, in.Id).Scan(res)
if err != nil {
return nil, gerror.Wrap(err, "查询会员登录日志详情失败")
}
if res.Id == 0 {
return nil, gerror.New("会员登录日志不存在")
}
res.Os = useragent.GetOs(res.UserAgent)
res.Browser = useragent.GetBrowser(res.UserAgent)
return
}
func (s *sSysAppMemberLoginLog) Export(ctx context.Context, in *sysin.AppMemberLoginLogListInp) error {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return err
}
var exports []sysin.AppMemberLoginLogExportModel
if err = gconv.Scan(list, &exports); err != nil {
return err
}
tags, err := convert.GetEntityDescTags(sysin.AppMemberLoginLogExportModel{})
if err != nil {
return err
}
fileName := "导出会员登录日志-" + gctx.CtxId(ctx)
sheetName := fmt.Sprintf("共%v行,共%v页,当前第%v页,本页%v行", totalCount, form.CalPageCount(totalCount, in.PerPage), in.Page, len(exports))
return excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
}
func (s *sSysAppMemberLoginLog) Push(ctx context.Context, in *sysin.AppMemberLoginLogPushInp) {
r := ghttp.RequestFromCtx(ctx)
if r == nil {
g.Log().Warningf(ctx, "会员登录日志缺少HTTP请求上下文")
return
}
model := entity.AppMemberLoginLog{
ReqId: gctx.CtxId(ctx),
LoginType: in.LoginType,
Status: consts.StatusEnabled,
LoginAt: gtime.Now(),
UserAgent: r.UserAgent(),
}
if in.Member != nil {
model.TenantId = in.Member.TenantId
model.MemberId = in.Member.Id
model.Username = in.Member.Username
model.MobileMask = encrypt.MaskMobile(in.Member.Mobile)
} else if in.LoginType == consts.AppMemberLoginTypeSms {
model.MobileMask = encrypt.MaskMobile(in.Account)
} else {
model.Username = strings.TrimSpace(in.Account)
}
if in.Err != nil {
model.Status = consts.StatusDisable
model.ErrMsg = normalizeLoginError(in.Err)
}
clientIP := location.GetClientIp(r)
model.LoginIpMask = maskIP(clientIP)
if cipher, err := encrypt.EncryptField(ctx, clientIP); err == nil {
model.LoginIpCipher = cipher
} else {
g.Log().Warningf(ctx, "加密会员登录IP失败 memberId:%d err:%+v", model.MemberId, err)
}
if ipData, err := location.GetLocation(ctx, clientIP); err == nil && ipData != nil {
model.LoginRegion = formatLoginRegion(ipData)
} else if err != nil {
g.Log().Warningf(ctx, "解析会员登录地区失败 memberId:%d ip:%s err:%+v", model.MemberId, model.LoginIpMask, err)
}
origin := r.Header.Get("Origin")
if domain, _, _ := resolveRequestDomain(origin, r.Host); domain != "" {
model.LoginDomain = domain
}
if err := queue.Push(consts.QueueAppMemberLoginLogTopic, model); err != nil {
g.Log().Warningf(ctx, "推送会员登录日志失败,改为同步写入 memberId:%d err:%+v", model.MemberId, err)
if writeErr := s.RealWrite(ctx, model); writeErr != nil {
g.Log().Warningf(ctx, "同步写入会员登录日志失败 memberId:%d err:%+v", model.MemberId, writeErr)
}
}
}
func (s *sSysAppMemberLoginLog) RealWrite(ctx context.Context, model entity.AppMemberLoginLog) error {
_, err := dao.AppMemberLoginLog.Ctx(ctx).Data(model).OmitEmptyData().Insert()
if err != nil {
return gerror.Wrap(err, "写入会员登录日志失败")
}
return nil
}
func maskIP(value string) string {
ip := net.ParseIP(strings.TrimSpace(value))
if ip == nil {
return ""
}
if v4 := ip.To4(); v4 != nil {
return fmt.Sprintf("%d.%d.*.*", v4[0], v4[1])
}
parts := strings.Split(ip.String(), ":")
if len(parts) > 4 {
return strings.Join(parts[:4], ":") + ":****"
}
return ip.String()
}
func formatLoginRegion(data *location.IpLocationData) string {
parts := []string{data.Country, data.Province, data.City, data.Area}
seen := make(map[string]struct{})
result := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if _, ok := seen[part]; ok {
continue
}
seen[part] = struct{}{}
result = append(result, part)
}
if len(result) == 0 {
return strings.TrimSpace(data.Region)
}
return strings.Join(result, " ")
}
func normalizeLoginError(err error) string {
if err == nil {
return ""
}
message := err.Error()
allowed := []string{"账号或密码错误", "验证码错误或已过期", "会员不存在", "会员已禁用", "风控限制登录", "登录失败,请稍后重试"}
for _, item := range allowed {
if strings.Contains(message, item) {
return item
}
}
return "系统异常"
}
+448
View File
@@ -0,0 +1,448 @@
package sys
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net"
"net/url"
"strings"
"unicode/utf8"
"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/xuri/excelize/v2"
)
type sSysUrlPool struct{}
func NewSysUrlPool() *sSysUrlPool { return &sSysUrlPool{} }
func init() { service.RegisterSysUrlPool(NewSysUrlPool()) }
func (s *sSysUrlPool) Model(ctx context.Context) *gdb.Model {
return handler.Model(dao.UrlPool.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysUrlPool) bindingModel(ctx context.Context) *gdb.Model {
return handler.Model(dao.MemberUrlBinding.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysUrlPool) List(ctx context.Context, in *sysin.UrlPoolListInp) (list []*sysin.UrlPoolListModel, totalCount int, err error) {
pc, bc := dao.UrlPool.Columns(), dao.MemberUrlBinding.Columns()
mod := s.Model(ctx).As("p")
if in.Name != "" {
mod = mod.WhereLike("p."+pc.Name, "%"+in.Name+"%")
}
if in.Domain != "" {
mod = mod.WhereLike("p."+pc.Domain, "%"+strings.ToLower(strings.TrimSpace(in.Domain))+"%")
}
if in.Status > 0 {
mod = mod.Where("p."+pc.Status, in.Status)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween("p."+pc.CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
totalCount, err = mod.Clone().Count()
if err != nil || totalCount == 0 {
return
}
mod = mod.Fields(
"p."+pc.Id, "p."+pc.Name, "p."+pc.Scheme, "p."+pc.Domain, "p."+pc.BaseUrl,
"p."+pc.Status, "p."+pc.BindCount, "p."+pc.LastBoundAt, "p."+pc.Remark,
"p."+pc.CreatedAt, "p."+pc.UpdatedAt,
fmt.Sprintf("(SELECT COUNT(*) FROM %s b WHERE b.%s=p.%s) current_bound_count", dao.MemberUrlBinding.Table(), bc.UrlPoolId, pc.Id),
)
err = mod.Page(in.Page, in.PerPage).OrderAsc("p." + pc.BindCount).OrderAsc("p." + pc.Id).Scan(&list)
return
}
func (s *sSysUrlPool) View(ctx context.Context, in *sysin.UrlPoolViewInp) (res *sysin.UrlPoolViewModel, err error) {
pc, bc := dao.UrlPool.Columns(), dao.MemberUrlBinding.Columns()
res = new(sysin.UrlPoolViewModel)
err = s.Model(ctx).As("p").Fields(
"p.*",
fmt.Sprintf("(SELECT COUNT(*) FROM %s b WHERE b.%s=p.%s) current_bound_count", dao.MemberUrlBinding.Table(), bc.UrlPoolId, pc.Id),
).Where("p."+pc.Id, in.Id).Scan(res)
if err != nil {
return nil, gerror.Wrap(err, "查询URL地址详情失败")
}
if res.Id == 0 {
return nil, gerror.New("URL地址不存在")
}
return
}
func (s *sSysUrlPool) Edit(ctx context.Context, in *sysin.UrlPoolEditInp) (err error) {
scheme, domain, baseURL, err := normalizeRegisterURL(in.BaseUrl)
if err != nil {
return err
}
hash := domainHash(domain)
pc := dao.UrlPool.Columns()
var before *entity.UrlPool
if in.Id > 0 {
if err = s.Model(ctx).WherePri(in.Id).Scan(&before); err != nil {
return gerror.Wrap(err, "读取URL地址失败")
}
if before == nil {
return gerror.New("URL地址不存在")
}
if before.Domain != domain {
count, e := s.bindingModel(ctx).Where(dao.MemberUrlBinding.Columns().UrlPoolId, in.Id).Count()
if e != nil {
return gerror.Wrap(e, "检查URL地址绑定失败")
}
if count > 0 {
return gerror.New("该地址已绑定会员,不能修改域名")
}
}
}
count, err := s.Model(ctx).Where(pc.DomainHash, hash).WhereNot(pc.Id, in.Id).Count()
if err != nil {
return gerror.Wrap(err, "检查URL地址重复失败")
}
if count > 0 {
return gerror.New("该注册域名已存在")
}
data := g.Map{
pc.Name: in.Name, pc.Scheme: scheme, pc.Domain: domain, pc.DomainHash: hash,
pc.BaseUrl: baseURL, pc.Status: in.Status, pc.Remark: in.Remark,
pc.UpdatedBy: contexts.GetUserId(ctx),
}
if in.Id > 0 {
_, err = s.Model(ctx).Fields(pc.Name, pc.Scheme, pc.Domain, pc.DomainHash, pc.BaseUrl, pc.Status, pc.Remark, pc.UpdatedBy).WherePri(in.Id).Data(data).Hook(hook.SaveTenant).Update()
} else {
data[pc.CreatedBy] = contexts.GetUserId(ctx)
result, e := dao.UrlPool.Ctx(ctx).Fields(pc.Name, pc.Scheme, pc.Domain, pc.DomainHash, pc.BaseUrl, pc.Status, pc.Remark, pc.CreatedBy, pc.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
if e != nil {
err = e
} else {
id, _ := result.LastInsertId()
in.Id = uint64(id)
}
}
if err != nil {
return gerror.Wrap(err, "保存URL地址失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "url_pool", Action: "url_pool.edit", TargetType: "url_pool", TargetId: in.Id, Before: before, After: data})
}
func (s *sSysUrlPool) Status(ctx context.Context, in *sysin.UrlPoolStatusInp) error {
pc := dao.UrlPool.Columns()
var before *entity.UrlPool
if err := s.Model(ctx).WherePri(in.Id).Scan(&before); err != nil {
return gerror.Wrap(err, "读取URL地址失败")
}
if before == nil {
return gerror.New("URL地址不存在")
}
if before.Status == in.Status {
return nil
}
if _, err := s.Model(ctx).Fields(pc.Status, pc.UpdatedBy).WherePri(in.Id).Data(g.Map{pc.Status: in.Status, pc.UpdatedBy: contexts.GetUserId(ctx)}).Hook(hook.SaveTenant).Update(); err != nil {
return gerror.Wrap(err, "更新URL地址状态失败")
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "url_pool", Action: "url_pool.status", TargetType: "url_pool", TargetId: in.Id, Before: g.Map{"status": before.Status}, After: g.Map{"status": in.Status}})
}
func (s *sSysUrlPool) Import(ctx context.Context, reader io.Reader) (res *sysin.UrlPoolImportModel, err error) {
book, err := excelize.OpenReader(reader)
if err != nil {
return nil, gerror.New("Excel文件无法解析,请确认文件格式正确")
}
defer book.Close()
sheets := book.GetSheetList()
if len(sheets) == 0 {
return nil, gerror.New("Excel文件中没有工作表")
}
rows, err := book.GetRows(sheets[0])
if err != nil {
return nil, gerror.Wrap(err, "读取Excel内容失败")
}
if len(rows) < 2 {
return nil, gerror.New("Excel中没有可导入的数据")
}
nameColumn, urlColumn, remarkColumn := findUrlPoolImportColumns(rows[0])
if nameColumn < 0 || urlColumn < 0 {
return nil, gerror.New("Excel表头必须包含“地址名称”和“注册入口URL”")
}
type importCandidate struct {
domainHash string
data g.Map
}
res = &sysin.UrlPoolImportModel{Issues: make([]*sysin.UrlPoolImportIssue, 0)}
candidates := make([]importCandidate, 0, len(rows)-1)
fileDomains := make(map[string]struct{})
pc := dao.UrlPool.Columns()
for index := 1; index < len(rows); index++ {
row := rows[index]
name := importCell(row, nameColumn)
rawURL := importCell(row, urlColumn)
remark := importCell(row, remarkColumn)
if name == "" && rawURL == "" && remark == "" {
continue
}
res.TotalRows++
rowNumber := index + 1
if name == "" {
appendUrlPoolImportIssue(res, rowNumber, "地址名称不能为空")
continue
}
if utf8.RuneCountInString(name) > 100 {
appendUrlPoolImportIssue(res, rowNumber, "地址名称不能超过100个字符")
continue
}
if utf8.RuneCountInString(remark) > 500 {
appendUrlPoolImportIssue(res, rowNumber, "备注不能超过500个字符")
continue
}
scheme, domain, baseURL, normalizeErr := normalizeRegisterURL(rawURL)
if normalizeErr != nil {
appendUrlPoolImportIssue(res, rowNumber, normalizeErr.Error())
continue
}
if len(baseURL) > 512 {
appendUrlPoolImportIssue(res, rowNumber, "注册入口URL不能超过512个字符")
continue
}
hash := domainHash(domain)
if _, exists := fileDomains[hash]; exists {
res.DuplicateCount++
continue
}
fileDomains[hash] = struct{}{}
candidates = append(candidates, importCandidate{domainHash: hash, data: g.Map{
pc.Name: name, pc.Scheme: scheme, pc.Domain: domain, pc.DomainHash: hash,
pc.BaseUrl: baseURL, pc.Status: consts.UrlPoolStatusEnabled, pc.Remark: remark,
pc.CreatedBy: contexts.GetUserId(ctx), pc.UpdatedBy: contexts.GetUserId(ctx),
}})
}
if res.TotalRows > 5000 {
return nil, gerror.New("单次最多导入5000条地址")
}
if len(candidates) == 0 {
return res, nil
}
hashes := make([]string, 0, len(candidates))
for _, candidate := range candidates {
hashes = append(hashes, candidate.domainHash)
}
existingValues, err := s.Model(ctx).Fields(pc.DomainHash).WhereIn(pc.DomainHash, hashes).Array()
if err != nil {
return nil, gerror.Wrap(err, "检查重复注册域名失败")
}
existing := make(map[string]struct{}, len(existingValues))
for _, value := range existingValues {
existing[value.String()] = struct{}{}
}
insertRows := make(g.List, 0, len(candidates))
for _, candidate := range candidates {
if _, exists := existing[candidate.domainHash]; exists {
res.DuplicateCount++
continue
}
insertRows = append(insertRows, candidate.data)
}
if len(insertRows) == 0 {
return res, nil
}
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
_, insertErr := dao.UrlPool.Ctx(ctx).Fields(
pc.Name, pc.Scheme, pc.Domain, pc.DomainHash, pc.BaseUrl, pc.Status, pc.Remark, pc.CreatedBy, pc.UpdatedBy,
).Hook(hook.SaveTenant).Data(insertRows).Insert()
return insertErr
})
if err != nil {
return nil, gerror.Wrap(err, "批量导入URL地址失败")
}
res.SuccessCount = len(insertRows)
if auditErr := service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{
Module: "url_pool", Action: "url_pool.import", TargetType: "url_pool", After: g.Map{
"totalRows": res.TotalRows, "successCount": res.SuccessCount,
"duplicateCount": res.DuplicateCount, "failedCount": res.FailedCount,
},
}); auditErr != nil {
return nil, auditErr
}
return res, nil
}
func findUrlPoolImportColumns(header []string) (nameColumn, urlColumn, remarkColumn int) {
nameColumn, urlColumn, remarkColumn = -1, -1, -1
for index, value := range header {
value = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(value, "\ufeff")))
switch value {
case "地址名称", "名称", "name":
nameColumn = index
case "注册入口url", "注册入口", "入口url", "baseurl", "base_url":
urlColumn = index
case "备注", "remark":
remarkColumn = index
}
}
return
}
func importCell(row []string, column int) string {
if column < 0 || column >= len(row) {
return ""
}
return strings.TrimSpace(row[column])
}
func appendUrlPoolImportIssue(res *sysin.UrlPoolImportModel, row int, message string) {
res.FailedCount++
if len(res.Issues) < 100 {
res.Issues = append(res.Issues, &sysin.UrlPoolImportIssue{Row: row, Message: message})
}
}
func (s *sSysUrlPool) MemberList(ctx context.Context, in *sysin.UrlPoolMemberListInp) (list []*sysin.UrlPoolMemberListModel, totalCount int, err error) {
bc, mc := dao.MemberUrlBinding.Columns(), dao.AppMember.Columns()
mod := s.bindingModel(ctx).As("b").LeftJoin(dao.AppMember.Table()+" m", "m."+mc.Id+"=b."+bc.MemberId).Where("b."+bc.UrlPoolId, in.UrlPoolId)
if in.Keyword != "" {
like := "%" + strings.TrimSpace(in.Keyword) + "%"
mod = mod.Where(fmt.Sprintf("(m.%s LIKE ? OR m.%s LIKE ? OR m.%s LIKE ?)", mc.MemberNo, mc.Username, mc.Mobile), like, like, like)
}
if len(in.BoundAt) == 2 {
mod = mod.WhereBetween("b."+bc.BoundAt, in.BoundAt[0], in.BoundAt[1])
}
totalCount, err = mod.Clone().Count()
if err != nil || totalCount == 0 {
return
}
mod = mod.Fields(
"b."+bc.MemberId, "m."+mc.MemberNo, "m."+mc.Username, "m."+mc.Mobile, "m."+mc.MemberStatus, "b."+bc.BoundAt,
)
err = mod.Page(in.Page, in.PerPage).OrderDesc("b." + bc.Id).Scan(&list)
for _, row := range list {
row.Mobile = encrypt.MaskMobile(row.Mobile)
}
return
}
func (s *sSysUrlPool) Summary(ctx context.Context, in *sysin.UrlPoolSummaryInp) (res *sysin.UrlPoolSummaryModel, err error) {
res = new(sysin.UrlPoolSummaryModel)
pc, bc, mc := dao.UrlPool.Columns(), dao.MemberUrlBinding.Columns(), dao.AppMember.Columns()
if res.TotalCount, err = s.Model(ctx).Count(); err != nil {
return nil, err
}
res.EnabledCount, err = s.Model(ctx).Where(pc.Status, consts.UrlPoolStatusEnabled).Count()
if err != nil {
return nil, err
}
res.InvalidCount, err = s.Model(ctx).Where(pc.Status, consts.UrlPoolStatusInvalid).Count()
if err != nil {
return nil, err
}
res.BoundMemberCount, err = s.bindingModel(ctx).Count()
if err != nil {
return nil, err
}
res.UnboundMemberCount, err = handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).As("m").LeftJoin(dao.MemberUrlBinding.Table()+" b", "b."+bc.MemberId+"=m."+mc.Id).WhereNull("b." + bc.Id).Count()
if err != nil {
return nil, err
}
res.InvalidMemberCount, err = s.bindingModel(ctx).As("b").InnerJoin(dao.UrlPool.Table()+" p", "p."+pc.Id+"=b."+bc.UrlPoolId).Where("p."+pc.Status, consts.UrlPoolStatusInvalid).Count()
return
}
func (s *sSysUrlPool) BindOnRegister(ctx context.Context, memberId uint64, origin, host string) (bound bool, domain string, err error) {
domain, normalizedOrigin, err := resolveRequestDomain(origin, host)
if err != nil || domain == "" {
return false, "", nil
}
pc, bc := dao.UrlPool.Columns(), dao.MemberUrlBinding.Columns()
var pool *entity.UrlPool
err = s.Model(ctx).Where(pc.DomainHash, domainHash(domain)).Where(pc.Status, consts.UrlPoolStatusEnabled).Scan(&pool)
if err != nil {
return false, domain, gerror.Wrap(err, "查询注册域名失败")
}
if pool == nil {
return false, domain, nil
}
var existing *entity.MemberUrlBinding
if err = s.bindingModel(ctx).Where(bc.MemberId, memberId).Scan(&existing); err != nil {
return false, domain, gerror.Wrap(err, "查询会员域名绑定失败")
}
if existing != nil {
return existing.UrlPoolId == pool.Id, existing.DomainSnapshot, nil
}
now := gtime.Now()
if _, err = dao.MemberUrlBinding.Ctx(ctx).Fields(bc.TenantId, bc.MemberId, bc.UrlPoolId, bc.DomainSnapshot, bc.OriginSnapshot, bc.BoundAt).Data(g.Map{bc.TenantId: pool.TenantId, bc.MemberId: memberId, bc.UrlPoolId: pool.Id, bc.DomainSnapshot: domain, bc.OriginSnapshot: normalizedOrigin, bc.BoundAt: now}).Insert(); err != nil {
return false, domain, gerror.Wrap(err, "创建会员域名绑定失败")
}
if _, err = s.Model(ctx).Fields(pc.BindCount, pc.LastBoundAt).WherePri(pool.Id).Data(g.Map{pc.BindCount: gdb.Raw(pc.BindCount + "+1"), pc.LastBoundAt: now}).Update(); err != nil {
return false, domain, gerror.Wrap(err, "更新域名绑定次数失败")
}
return true, domain, nil
}
func normalizeRegisterURL(raw string) (scheme, domain, baseURL string, err error) {
raw = strings.TrimSpace(raw)
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" {
return "", "", "", gerror.New("注册入口URL格式不正确")
}
if u.Scheme != "http" && u.Scheme != "https" {
return "", "", "", gerror.New("注册入口只支持HTTP或HTTPS")
}
if u.User != nil || u.RawQuery != "" || u.Fragment != "" {
return "", "", "", gerror.New("注册入口不能包含账号、查询参数或片段")
}
hostname := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(u.Hostname())), ".")
if hostname == "" {
return "", "", "", gerror.New("注册域名不能为空")
}
port := u.Port()
if (u.Scheme == "https" && port == "443") || (u.Scheme == "http" && port == "80") {
port = ""
}
domain = hostname
if port != "" {
domain = net.JoinHostPort(hostname, port)
}
path := strings.TrimSpace(u.EscapedPath())
if path == "" {
path = "/register"
}
return strings.ToLower(u.Scheme), domain, strings.ToLower(u.Scheme) + "://" + domain + path, nil
}
func resolveRequestDomain(origin, host string) (domain, normalizedOrigin string, err error) {
if strings.TrimSpace(origin) != "" {
u, e := url.Parse(strings.TrimSpace(origin))
if e == nil && u.Scheme != "" && u.Host != "" && u.User == nil {
_, domain, normalizedOrigin, e = normalizeRegisterURL(u.Scheme + "://" + u.Host + "/register")
return domain, strings.TrimSuffix(normalizedOrigin, "/register"), e
}
}
host = strings.TrimSpace(host)
if host == "" {
return "", "", nil
}
_, domain, normalizedOrigin, err = normalizeRegisterURL("https://" + host + "/register")
return domain, strings.TrimSuffix(normalizedOrigin, "/register"), err
}
func domainHash(domain string) string {
sum := sha256.Sum256([]byte(strings.ToLower(strings.TrimSpace(domain))))
return hex.EncodeToString(sum[:])
}
@@ -0,0 +1,58 @@
package sys
import "testing"
func TestNormalizeRegisterURL(t *testing.T) {
tests := []struct {
name string
raw string
domain string
baseURL string
wantErr bool
}{
{name: "default register path", raw: "https://Example.COM", domain: "example.com", baseURL: "https://example.com/register"},
{name: "keep custom port", raw: "http://example.com:8080/signup", domain: "example.com:8080", baseURL: "http://example.com:8080/signup"},
{name: "drop default port", raw: "https://example.com:443/register", domain: "example.com", baseURL: "https://example.com/register"},
{name: "reject query", raw: "https://example.com/register?code=1", wantErr: true},
{name: "reject non http", raw: "ftp://example.com/register", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, domain, baseURL, err := normalizeRegisterURL(tt.raw)
if tt.wantErr {
if err == nil {
t.Fatalf("normalizeRegisterURL(%q) expected error", tt.raw)
}
return
}
if err != nil {
t.Fatalf("normalizeRegisterURL(%q) returned error: %v", tt.raw, err)
}
if domain != tt.domain || baseURL != tt.baseURL {
t.Fatalf("normalizeRegisterURL(%q) = domain %q, baseURL %q", tt.raw, domain, baseURL)
}
})
}
}
func TestResolveRequestDomainPrefersOrigin(t *testing.T) {
domain, origin, err := resolveRequestDomain("https://REGISTER.example.com", "ignored.example.com")
if err != nil {
t.Fatal(err)
}
if domain != "register.example.com" || origin != "https://register.example.com" {
t.Fatalf("unexpected domain resolution: %q %q", domain, origin)
}
}
func TestFindUrlPoolImportColumns(t *testing.T) {
nameColumn, urlColumn, remarkColumn := findUrlPoolImportColumns([]string{"\ufeff地址名称", "注册入口URL", "备注"})
if nameColumn != 0 || urlColumn != 1 || remarkColumn != 2 {
t.Fatalf("unexpected import columns: name=%d url=%d remark=%d", nameColumn, urlColumn, remarkColumn)
}
nameColumn, urlColumn, remarkColumn = findUrlPoolImportColumns([]string{"NAME", "base_url"})
if nameColumn != 0 || urlColumn != 1 || remarkColumn != -1 {
t.Fatalf("unexpected alias columns: name=%d url=%d remark=%d", nameColumn, urlColumn, remarkColumn)
}
}
@@ -0,0 +1,31 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// AppMemberLoginLog is the golang structure of table hg_app_member_login_log for DAO operations like Where/Data.
type AppMemberLoginLog struct {
g.Meta `orm:"table:hg_app_member_login_log, do:true"`
Id any // 日志ID
TenantId any // 租户ID
ReqId any // 请求ID
MemberId any // 会员ID,未识别会员时为0
Username any // 用户名快照
MobileMask any // 手机号脱敏值
LoginType any // 登录方式:1账号密码,2手机验证码
Status any // 登录状态:1成功,2失败
ErrMsg any // 失败原因
LoginAt *gtime.Time // 登录时间
LoginIpCipher any // 登录IP密文
LoginIpMask any // 登录IP脱敏值
LoginRegion any // 登录地区
LoginDomain any // 登录页面标准化域名
UserAgent any // 浏览器UA
CreatedAt *gtime.Time // 创建时间
}
@@ -0,0 +1,24 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// MemberUrlBinding is the golang structure of table hg_member_url_binding for DAO operations like Where/Data.
type MemberUrlBinding struct {
g.Meta `orm:"table:hg_member_url_binding, do:true"`
Id any // 主键
TenantId any // 租户ID
MemberId any // 会员ID
UrlPoolId any // URL地址池ID
DomainSnapshot any // 绑定时域名快照
OriginSnapshot any // 绑定时Origin快照
BoundAt *gtime.Time // 绑定时间
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
}
+32
View File
@@ -0,0 +1,32 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// UrlPool is the golang structure of table hg_url_pool for DAO operations like Where/Data.
type UrlPool struct {
g.Meta `orm:"table:hg_url_pool, do:true"`
Id any // 主键
TenantId any // 租户ID
Name any // 地址名称
Scheme any // 协议:http或https
Domain any // 标准化域名,可含非默认端口
DomainHash any // 标准化域名SHA256
BaseUrl any // 注册入口基础地址
Status any // 状态:1启用,2失效
BindCount any // 累计成功绑定次数
LastBoundAt *gtime.Time // 最后绑定时间
Remark any // 备注
CreatedBy any // 创建者ID
UpdatedBy any // 更新者ID
DeletedBy any // 删除者ID
CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间
DeletedAt *gtime.Time // 删除时间
}
@@ -0,0 +1,29 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// AppMemberLoginLog is the golang structure for table app_member_login_log.
type AppMemberLoginLog struct {
Id uint64 `json:"id" orm:"id" description:"日志ID"`
TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"`
ReqId string `json:"reqId" orm:"req_id" description:"请求ID"`
MemberId uint64 `json:"memberId" orm:"member_id" description:"会员ID,未识别会员时为0"`
Username string `json:"username" orm:"username" description:"用户名快照"`
MobileMask string `json:"mobileMask" orm:"mobile_mask" description:"手机号脱敏值"`
LoginType int `json:"loginType" orm:"login_type" description:"登录方式:1账号密码,2手机验证码"`
Status int `json:"status" orm:"status" description:"登录状态:1成功,2失败"`
ErrMsg string `json:"errMsg" orm:"err_msg" description:"失败原因"`
LoginAt *gtime.Time `json:"loginAt" orm:"login_at" description:"登录时间"`
LoginIpCipher string `json:"loginIpCipher" orm:"login_ip_cipher" description:"登录IP密文"`
LoginIpMask string `json:"loginIpMask" orm:"login_ip_mask" description:"登录IP脱敏值"`
LoginRegion string `json:"loginRegion" orm:"login_region" description:"登录地区"`
LoginDomain string `json:"loginDomain" orm:"login_domain" description:"登录页面标准化域名"`
UserAgent string `json:"userAgent" orm:"user_agent" description:"浏览器UA"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
}
@@ -0,0 +1,22 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// MemberUrlBinding is the golang structure for table member_url_binding.
type MemberUrlBinding struct {
Id uint64 `json:"id" orm:"id" description:"主键"`
TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"`
MemberId uint64 `json:"memberId" orm:"member_id" description:"会员ID"`
UrlPoolId uint64 `json:"urlPoolId" orm:"url_pool_id" description:"URL地址池ID"`
DomainSnapshot string `json:"domainSnapshot" orm:"domain_snapshot" description:"绑定时域名快照"`
OriginSnapshot string `json:"originSnapshot" orm:"origin_snapshot" description:"绑定时Origin快照"`
BoundAt *gtime.Time `json:"boundAt" orm:"bound_at" description:"绑定时间"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"`
}
+30
View File
@@ -0,0 +1,30 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// UrlPool is the golang structure for table url_pool.
type UrlPool struct {
Id uint64 `json:"id" orm:"id" description:"主键"`
TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"`
Name string `json:"name" orm:"name" description:"地址名称"`
Scheme string `json:"scheme" orm:"scheme" description:"协议:http或https"`
Domain string `json:"domain" orm:"domain" description:"标准化域名,可含非默认端口"`
DomainHash string `json:"domainHash" orm:"domain_hash" description:"标准化域名SHA256"`
BaseUrl string `json:"baseUrl" orm:"base_url" description:"注册入口基础地址"`
Status int `json:"status" orm:"status" description:"状态:1启用,2失效"`
BindCount uint64 `json:"bindCount" orm:"bind_count" description:"累计成功绑定次数"`
LastBoundAt *gtime.Time `json:"lastBoundAt" orm:"last_bound_at" description:"最后绑定时间"`
Remark string `json:"remark" orm:"remark" description:"备注"`
CreatedBy uint64 `json:"createdBy" orm:"created_by" description:"创建者ID"`
UpdatedBy uint64 `json:"updatedBy" orm:"updated_by" description:"更新者ID"`
DeletedBy uint64 `json:"deletedBy" orm:"deleted_by" description:"删除者ID"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"`
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"删除时间"`
}
@@ -21,6 +21,8 @@ type AppMemberListInp struct {
MemberStatus int `json:"memberStatus" dc:"会员状态"`
AutoDispatch int `json:"autoDispatch" dc:"派单模式"`
MemberCategory int `json:"memberCategory" dc:"会员类别"`
RegisterDomain string `json:"registerDomain" dc:"注册域名(模糊查询)"`
DomainStatus int `json:"domainStatus" dc:"域名绑定状态"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"注册时间"`
}
@@ -47,6 +49,10 @@ type AppMemberListModel struct {
LastLoginRegion string `json:"lastLoginRegion"`
TodayTradeCount int `json:"todayTradeCount"`
CreatedAt *gtime.Time `json:"createdAt"`
UrlPoolId uint64 `json:"urlPoolId"`
RegisterDomain string `json:"registerDomain"`
DomainStatus int `json:"domainStatus"`
DomainBoundAt *gtime.Time `json:"domainBoundAt"`
}
type AppMemberViewInp struct {
@@ -85,11 +91,8 @@ type AppMemberEditInp struct {
}
func (in *AppMemberEditInp) Filter(ctx context.Context) error {
if in.Id == 0 && strings.TrimSpace(in.Mobile) == "" {
return gerror.New("新增会员时手机号不能为空")
}
if in.Id == 0 && len(in.Password) < 8 {
return gerror.New("新增会员密码不能少于8位")
if in.Id == 0 {
return gerror.New("后台不支持创建会员,请由会员在C端完成注册")
}
if in.Password != "" && (len(in.Password) < 8 || !containsLetterAndNumber(in.Password)) {
return gerror.New("密码至少8位且必须同时包含字母和数字")
@@ -0,0 +1,65 @@
package sysin
import (
"context"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
)
type AppMemberRegisterInp struct {
Username string `json:"username" v:"required|length:3,64#用户名不能为空|用户名长度为3到64个字符"`
Mobile string `json:"mobile" v:"required|phone-loose#手机号不能为空|手机号格式不正确"`
Password string `json:"password" v:"required|length:8,64#密码不能为空|密码长度为8到64个字符"`
InviteCode string `json:"inviteCode" v:"max-length:32#邀请码不能超过32个字符"`
RegisterOrigin string `json:"registerOrigin" v:"max-length:512#注册来源不能超过512个字符"`
RegisterHost string `json:"registerHost" v:"max-length:255#注册域名不能超过255个字符"`
}
func (in *AppMemberRegisterInp) Filter(ctx context.Context) error {
in.Username = strings.TrimSpace(in.Username)
in.Mobile = strings.TrimSpace(in.Mobile)
in.InviteCode = strings.TrimSpace(in.InviteCode)
if !containsLetterAndNumber(in.Password) {
return gerror.New("密码至少8位且必须同时包含字母和数字")
}
return nil
}
type AppMemberRegisterModel struct {
MemberId uint64 `json:"memberId"`
MemberNo string `json:"memberNo"`
Bound bool `json:"bound"`
Domain string `json:"domain"`
}
type AppMemberAccountLoginInp struct {
Account string `json:"account" v:"required|max-length:64#登录账号不能为空|登录账号不能超过64个字符"`
Password string `json:"password" v:"required|max-length:64#登录密码不能为空|登录密码不能超过64个字符"`
LoginHost string `json:"loginHost" v:"max-length:255#登录域名不能超过255个字符"`
}
func (in *AppMemberAccountLoginInp) Filter(ctx context.Context) error {
in.Account = strings.TrimSpace(in.Account)
return nil
}
type AppMemberMobileLoginInp struct {
Mobile string `json:"mobile" v:"required|phone-loose#手机号不能为空|手机号格式不正确"`
Code string `json:"code" v:"required|length:4,8#验证码不能为空|验证码格式不正确"`
LoginHost string `json:"loginHost" v:"max-length:255#登录域名不能超过255个字符"`
}
func (in *AppMemberMobileLoginInp) Filter(ctx context.Context) error {
in.Mobile = strings.TrimSpace(in.Mobile)
return nil
}
type AppMemberLoginModel struct {
Id uint64 `json:"id"`
MemberNo string `json:"memberNo"`
Username string `json:"username"`
Mobile string `json:"mobile"`
Token string `json:"token"`
Expires int64 `json:"expires"`
}
@@ -0,0 +1,85 @@
package sysin
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/library/dict"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type AppMemberLoginLogListInp struct {
form.PageReq
Keyword string `json:"keyword" dc:"会员编号、用户名或手机号"`
LoginType int `json:"loginType" dc:"登录方式"`
Status int `json:"status" dc:"登录状态"`
LoginDomain string `json:"loginDomain" dc:"登录域名"`
LoginIp string `json:"loginIp" dc:"登录IP脱敏值"`
LoginAt []*gtime.Time `json:"loginAt" dc:"登录时间"`
}
func (in *AppMemberLoginLogListInp) Filter(ctx context.Context) error {
if in.LoginType > 0 && !dict.HasOptionKey(consts.AppMemberLoginTypeOptions, in.LoginType) {
return gerror.New("登录方式不合法")
}
if in.Status > 0 && in.Status != consts.StatusEnabled && in.Status != consts.StatusDisable {
return gerror.New("登录状态不合法")
}
return nil
}
type AppMemberLoginLogListModel struct {
Id uint64 `json:"id"`
ReqId string `json:"reqId"`
MemberId uint64 `json:"memberId"`
MemberNo string `json:"memberNo"`
Username string `json:"username"`
MobileMask string `json:"mobileMask"`
LoginType int `json:"loginType"`
Status int `json:"status"`
ErrMsg string `json:"errMsg"`
LoginAt *gtime.Time `json:"loginAt"`
LoginIpMask string `json:"loginIpMask"`
LoginRegion string `json:"loginRegion"`
LoginDomain string `json:"loginDomain"`
UserAgent string `json:"userAgent"`
Os string `json:"os"`
Browser string `json:"browser"`
}
type AppMemberLoginLogViewInp struct {
Id uint64 `json:"id" v:"required|min:1#日志ID不能为空|日志ID不能为空"`
}
func (in *AppMemberLoginLogViewInp) Filter(ctx context.Context) error { return nil }
type AppMemberLoginLogViewModel struct {
AppMemberLoginLogListModel
}
type AppMemberLoginLogExportModel struct {
Id uint64 `json:"id" description:"日志ID"`
MemberNo string `json:"memberNo" description:"会员编号"`
Username string `json:"username" description:"用户名"`
MobileMask string `json:"mobileMask" description:"手机号"`
LoginType int `json:"loginType" description:"登录方式"`
Status int `json:"status" description:"登录状态"`
LoginAt *gtime.Time `json:"loginAt" description:"登录时间"`
LoginIpMask string `json:"loginIpMask" description:"登录IP"`
LoginRegion string `json:"loginRegion" description:"登录地区"`
LoginDomain string `json:"loginDomain" description:"登录域名"`
Os string `json:"os" description:"操作系统"`
Browser string `json:"browser" description:"浏览器"`
ErrMsg string `json:"errMsg" description:"失败原因"`
}
type AppMemberLoginLogPushInp struct {
Member *entity.AppMember
LoginType int
Account string
Err error
}
@@ -0,0 +1,128 @@
package sysin
import (
"context"
"strings"
"hotgo/internal/consts"
"hotgo/internal/library/dict"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type UrlPoolListInp struct {
form.PageReq
Name string `json:"name" dc:"地址名称"`
Domain string `json:"domain" dc:"域名关键词"`
Status int `json:"status" dc:"状态"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
}
func (in *UrlPoolListInp) Filter(ctx context.Context) error {
if in.Status > 0 && !dict.HasOptionKey(consts.UrlPoolStatusOptions, in.Status) {
return gerror.New("URL地址状态不合法")
}
return nil
}
type UrlPoolListModel struct {
Id uint64 `json:"id"`
Name string `json:"name"`
Scheme string `json:"scheme"`
Domain string `json:"domain"`
BaseUrl string `json:"baseUrl"`
Status int `json:"status"`
BindCount uint64 `json:"bindCount"`
CurrentBoundCount uint64 `json:"currentBoundCount"`
LastBoundAt *gtime.Time `json:"lastBoundAt"`
Remark string `json:"remark"`
CreatedAt *gtime.Time `json:"createdAt"`
UpdatedAt *gtime.Time `json:"updatedAt"`
}
type UrlPoolViewInp struct {
Id uint64 `json:"id" v:"required|min:1#地址ID不能为空|地址ID不能为空"`
}
func (in *UrlPoolViewInp) Filter(ctx context.Context) error { return nil }
type UrlPoolViewModel struct {
UrlPoolListModel
CreatedBy uint64 `json:"createdBy"`
UpdatedBy uint64 `json:"updatedBy"`
}
type UrlPoolEditInp struct {
Id uint64 `json:"id"`
Name string `json:"name" v:"required|max-length:100#地址名称不能为空|地址名称不能超过100个字符"`
BaseUrl string `json:"baseUrl" v:"required|max-length:512#注册入口不能为空|注册入口不能超过512个字符"`
Status int `json:"status" v:"required#状态不能为空"`
Remark string `json:"remark" v:"max-length:500#备注不能超过500个字符"`
}
func (in *UrlPoolEditInp) Filter(ctx context.Context) error {
in.Name = strings.TrimSpace(in.Name)
in.BaseUrl = strings.TrimSpace(in.BaseUrl)
if !dict.HasOptionKey(consts.UrlPoolStatusOptions, in.Status) {
return gerror.New("URL地址状态不合法")
}
return nil
}
type UrlPoolStatusInp struct {
Id uint64 `json:"id" v:"required|min:1#地址ID不能为空|地址ID不能为空"`
Status int `json:"status" v:"required#状态不能为空"`
}
func (in *UrlPoolStatusInp) Filter(ctx context.Context) error {
if !dict.HasOptionKey(consts.UrlPoolStatusOptions, in.Status) {
return gerror.New("URL地址状态不合法")
}
return nil
}
type UrlPoolImportIssue struct {
Row int `json:"row" dc:"Excel行号"`
Message string `json:"message" dc:"失败原因"`
}
type UrlPoolImportModel struct {
TotalRows int `json:"totalRows" dc:"有效数据行数"`
SuccessCount int `json:"successCount" dc:"成功导入数量"`
DuplicateCount int `json:"duplicateCount" dc:"重复跳过数量"`
FailedCount int `json:"failedCount" dc:"校验失败数量"`
Issues []*UrlPoolImportIssue `json:"issues" dc:"校验失败明细,最多返回100条"`
}
type UrlPoolMemberListInp struct {
form.PageReq
UrlPoolId uint64 `json:"urlPoolId" v:"required|min:1#地址ID不能为空|地址ID不能为空"`
Keyword string `json:"keyword" dc:"会员编号、用户名或手机号"`
BoundAt []*gtime.Time `json:"boundAt" dc:"绑定时间"`
}
func (in *UrlPoolMemberListInp) Filter(ctx context.Context) error { return nil }
type UrlPoolMemberListModel struct {
MemberId uint64 `json:"memberId"`
MemberNo string `json:"memberNo"`
Username string `json:"username"`
Mobile string `json:"mobile"`
MemberStatus int `json:"memberStatus"`
BoundAt *gtime.Time `json:"boundAt"`
}
type UrlPoolSummaryInp struct{}
func (in *UrlPoolSummaryInp) Filter(ctx context.Context) error { return nil }
type UrlPoolSummaryModel struct {
TotalCount int `json:"totalCount"`
EnabledCount int `json:"enabledCount"`
InvalidCount int `json:"invalidCount"`
BoundMemberCount int `json:"boundMemberCount"`
UnboundMemberCount int `json:"unboundMemberCount"`
InvalidMemberCount int `json:"invalidMemberCount"`
}
@@ -0,0 +1,27 @@
package queues
import (
"context"
"encoding/json"
"hotgo/internal/consts"
"hotgo/internal/library/queue"
"hotgo/internal/model/entity"
"hotgo/internal/service"
)
func init() { queue.RegisterConsumer(AppMemberLoginLog) }
var AppMemberLoginLog = &qAppMemberLoginLog{}
type qAppMemberLoginLog struct{}
func (q *qAppMemberLoginLog) GetTopic() string { return consts.QueueAppMemberLoginLogTopic }
func (q *qAppMemberLoginLog) Handle(ctx context.Context, mqMsg queue.MqMsg) error {
var data entity.AppMemberLoginLog
if err := json.Unmarshal(mqMsg.Body, &data); err != nil {
return err
}
return service.SysAppMemberLoginLog().RealWrite(ctx, data)
}
@@ -0,0 +1,5 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.AppMemberLoginLog) }
@@ -0,0 +1,5 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.UrlPool) }
+88 -35
View File
@@ -12,6 +12,7 @@ import (
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/utility/tree"
"io"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/database/gredis"
@@ -118,6 +119,28 @@ type (
NoticeEdit(ctx context.Context, in *sysin.MemberNoticeEditInp) error
NoticeRevoke(ctx context.Context, in *sysin.MemberNoticeRevokeInp) error
}
ISysUrlPool interface {
List(ctx context.Context, in *sysin.UrlPoolListInp) (list []*sysin.UrlPoolListModel, totalCount int, err error)
View(ctx context.Context, in *sysin.UrlPoolViewInp) (res *sysin.UrlPoolViewModel, err error)
Edit(ctx context.Context, in *sysin.UrlPoolEditInp) error
Status(ctx context.Context, in *sysin.UrlPoolStatusInp) error
Import(ctx context.Context, reader io.Reader) (res *sysin.UrlPoolImportModel, err error)
MemberList(ctx context.Context, in *sysin.UrlPoolMemberListInp) (list []*sysin.UrlPoolMemberListModel, totalCount int, err error)
Summary(ctx context.Context, in *sysin.UrlPoolSummaryInp) (res *sysin.UrlPoolSummaryModel, err error)
BindOnRegister(ctx context.Context, memberId uint64, origin, host string) (bound bool, domain string, err error)
}
ISysAppMemberLoginLog interface {
List(ctx context.Context, in *sysin.AppMemberLoginLogListInp) (list []*sysin.AppMemberLoginLogListModel, totalCount int, err error)
View(ctx context.Context, in *sysin.AppMemberLoginLogViewInp) (res *sysin.AppMemberLoginLogViewModel, err error)
Export(ctx context.Context, in *sysin.AppMemberLoginLogListInp) error
Push(ctx context.Context, in *sysin.AppMemberLoginLogPushInp)
RealWrite(ctx context.Context, model entity.AppMemberLoginLog) error
}
ISysAppMemberAuth interface {
Register(ctx context.Context, in *sysin.AppMemberRegisterInp) (res *sysin.AppMemberRegisterModel, err error)
AccountLogin(ctx context.Context, in *sysin.AppMemberAccountLoginInp) (res *sysin.AppMemberLoginModel, err error)
MobileLogin(ctx context.Context, in *sysin.AppMemberMobileLoginInp) (res *sysin.AppMemberLoginModel, err error)
}
ISysPortal interface {
SiteConfigCurrent(ctx context.Context, in *sysin.SiteConfigCurrentInp) (res *sysin.SiteConfigCurrentModel, err error)
SiteConfigHistory(ctx context.Context, in *sysin.SiteConfigHistoryInp) (list []*sysin.SiteConfigHistoryModel, totalCount int, err error)
@@ -547,41 +570,44 @@ type (
)
var (
localSysMemberLevel ISysMemberLevel
localSysTaskHall ISysTaskHall
localSysTradeOrder ISysTradeOrder
localSysRechargeOrder ISysRechargeOrder
localSysWithdrawalOrder ISysWithdrawalOrder
localSysCustomerService ISysCustomerService
localSysProduct ISysProduct
localSysTask ISysTask
localSysTaskSubmission ISysTaskSubmission
localSysAppMember ISysAppMember
localSysPortal ISysPortal
localSysDashboard ISysDashboard
localSysBizAudit ISysBizAudit
localSysApproval ISysApproval
localSysAddons ISysAddons
localSysAddonsConfig ISysAddonsConfig
localSysAttachment ISysAttachment
localSysBlacklist ISysBlacklist
localSysConfig ISysConfig
localSysCron ISysCron
localSysCronGroup ISysCronGroup
localSysCurdDemo ISysCurdDemo
localSysDictData ISysDictData
localSysDictType ISysDictType
localSysEmsLog ISysEmsLog
localSysGenCodes ISysGenCodes
localSysLog ISysLog
localSysLoginLog ISysLoginLog
localSysNormalTreeDemo ISysNormalTreeDemo
localSysOptionTreeDemo ISysOptionTreeDemo
localSysProvinces ISysProvinces
localSysServeLicense ISysServeLicense
localSysServeLog ISysServeLog
localSysSmsLog ISysSmsLog
localSysTestCategory ISysTestCategory
localSysMemberLevel ISysMemberLevel
localSysTaskHall ISysTaskHall
localSysTradeOrder ISysTradeOrder
localSysRechargeOrder ISysRechargeOrder
localSysWithdrawalOrder ISysWithdrawalOrder
localSysCustomerService ISysCustomerService
localSysProduct ISysProduct
localSysTask ISysTask
localSysTaskSubmission ISysTaskSubmission
localSysAppMember ISysAppMember
localSysUrlPool ISysUrlPool
localSysAppMemberLoginLog ISysAppMemberLoginLog
localSysAppMemberAuth ISysAppMemberAuth
localSysPortal ISysPortal
localSysDashboard ISysDashboard
localSysBizAudit ISysBizAudit
localSysApproval ISysApproval
localSysAddons ISysAddons
localSysAddonsConfig ISysAddonsConfig
localSysAttachment ISysAttachment
localSysBlacklist ISysBlacklist
localSysConfig ISysConfig
localSysCron ISysCron
localSysCronGroup ISysCronGroup
localSysCurdDemo ISysCurdDemo
localSysDictData ISysDictData
localSysDictType ISysDictType
localSysEmsLog ISysEmsLog
localSysGenCodes ISysGenCodes
localSysLog ISysLog
localSysLoginLog ISysLoginLog
localSysNormalTreeDemo ISysNormalTreeDemo
localSysOptionTreeDemo ISysOptionTreeDemo
localSysProvinces ISysProvinces
localSysServeLicense ISysServeLicense
localSysServeLog ISysServeLog
localSysSmsLog ISysSmsLog
localSysTestCategory ISysTestCategory
)
func SysMemberLevel() ISysMemberLevel {
@@ -658,6 +684,33 @@ func SysAppMember() ISysAppMember {
func RegisterSysAppMember(i ISysAppMember) { localSysAppMember = i }
func SysUrlPool() ISysUrlPool {
if localSysUrlPool == nil {
panic("implement not found for interface ISysUrlPool, forgot register?")
}
return localSysUrlPool
}
func RegisterSysUrlPool(i ISysUrlPool) { localSysUrlPool = i }
func SysAppMemberLoginLog() ISysAppMemberLoginLog {
if localSysAppMemberLoginLog == nil {
panic("implement not found for interface ISysAppMemberLoginLog, forgot register?")
}
return localSysAppMemberLoginLog
}
func RegisterSysAppMemberLoginLog(i ISysAppMemberLoginLog) { localSysAppMemberLoginLog = i }
func SysAppMemberAuth() ISysAppMemberAuth {
if localSysAppMemberAuth == nil {
panic("implement not found for interface ISysAppMemberAuth, forgot register?")
}
return localSysAppMemberAuth
}
func RegisterSysAppMemberAuth(i ISysAppMemberAuth) { localSysAppMemberAuth = i }
func SysPortal() ISysPortal {
if localSysPortal == nil {
panic("implement not found for interface ISysPortal, forgot register?")