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() 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)), 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), 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+"%") } if in.Mobile != "" { mod = mod.Where(col("m", mc.Mobile), in.Mobile) } 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 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]) } 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() 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)).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, "获取会员详情失败") } 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 { if in.Id == 0 { return gerror.New("后台不支持创建会员,请由会员在C端完成注册") } 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 != "" { count, err = s.memberModel(ctx).Where(mc.Mobile, in.Mobile).WhereNot(mc.Id, in.Id).Count() if err != nil { return err } if count > 0 { return gerror.New("手机号已存在") } data[mc.Mobile] = 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) inviteCode, e := newAppMemberInviteCode(ctx, s.memberModel(ctx)) if e != nil { return e } data[mc.InviteCode] = inviteCode data[mc.MemberStatus] = consts.AppMemberStatusNormal data[mc.CreatedBy] = userId result, e := s.memberModel(ctx).Fields(mc.MemberNo, mc.Username, mc.Mobile, 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"] = member.Mobile 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.Mobile), 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.Mobile), 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}) }