feat: 完善会员交易与任务管理流程

This commit is contained in:
richjohnrambo
2026-10-13 14:20:27 +08:00
parent df95801672
commit 40bfa4cba0
25 changed files with 1198 additions and 297 deletions
+2 -6
View File
@@ -25,9 +25,7 @@ type AppMemberColumns struct {
TenantId string // 租户ID
MemberNo string // 会员编号
Username string // 用户名
MobileCipher string // 手机号密文(AES-GCM)
MobileHash string // 手机号检索哈希(HMAC-SHA256)
MobileMask string // 手机号脱敏值
Mobile string //
PasswordHash string // 登录密码哈希(bcrypt)
TradePasswordHash string // 交易密码哈希
LevelId string // 会员等级ID
@@ -58,9 +56,7 @@ var appMemberColumns = AppMemberColumns{
TenantId: "tenant_id",
MemberNo: "member_no",
Username: "username",
MobileCipher: "mobile_cipher",
MobileHash: "mobile_hash",
MobileMask: "mobile_mask",
Mobile: "mobile",
PasswordHash: "password_hash",
TradePasswordHash: "trade_password_hash",
LevelId: "level_id",
+11 -25
View File
@@ -60,7 +60,7 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").
LeftJoin(dao.AppMember.Table()+" parent", col("parent", mc.Id)+"="+col("m", mc.ParentId)).
Fields(
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.MobileMask),
col("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),
@@ -72,11 +72,7 @@ func (s *sSysAppMember) List(ctx context.Context, in *sysin.AppMemberListInp) (l
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, e := encrypt.HashField(ctx, in.Mobile)
if e != nil {
return nil, 0, e
}
mod = mod.Where(col("m", mc.MobileHash), hash)
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
}
if in.MemberStatus > 0 {
mod = mod.Where(col("m", mc.MemberStatus), in.MemberStatus)
@@ -103,7 +99,7 @@ func (s *sSysAppMember) View(ctx context.Context, in *sysin.AppMemberViewInp) (r
mc, pc, wc := dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns()
res = new(sysin.AppMemberViewModel)
err = s.memberModel(ctx).As("m").LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("m", mc.Id)).LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").LeftJoin(dao.AppMember.Table()+" parent", col("parent", mc.Id)+"="+col("m", mc.ParentId)).Fields(
col("m", mc.Id), col("m", mc.MemberNo), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), col("p", pc.IdNoMask), col("p", pc.KycStatus), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), col("m", mc.ParentId), fmt.Sprintf("COALESCE(%s,'') parent_username", col("parent", mc.Username)), col("m", mc.InviteCode), col("m", mc.MemberStatus), col("m", mc.RiskStatus), col("m", mc.AutoDispatch), col("m", mc.IsTestAccount), fmt.Sprintf("CASE WHEN %s=1 THEN %d ELSE %d END member_category", col("m", mc.IsTestAccount), consts.AppMemberCategoryFake, consts.AppMemberCategoryReal), col("m", mc.LastLoginAt), col("m", mc.LastLoginIpMask), col("m", mc.LastLoginRegion), col("m", mc.CreatedAt), col("m", mc.LevelId), col("m", mc.Level), col("m", mc.CreditScore),
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),
).Where(col("m", mc.Id), in.Id).Scan(res)
if err != nil {
return nil, gerror.Wrap(err, "获取会员详情失败")
@@ -146,24 +142,14 @@ func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) er
}
data := g.Map{mc.Username: in.Username, mc.LevelId: in.LevelId, mc.ParentId: in.ParentId, mc.RiskStatus: in.RiskStatus, mc.CreditScore: in.CreditScore, mc.AutoDispatch: in.AutoDispatch, mc.IsTestAccount: isTestAccount, mc.UpdatedBy: userId}
if in.Mobile != "" {
cipher, e := encrypt.EncryptField(ctx, in.Mobile)
if e != nil {
return e
}
hash, e := encrypt.HashField(ctx, in.Mobile)
if e != nil {
return e
}
count, e = s.memberModel(ctx).Where(mc.MobileHash, hash).WhereNot(mc.Id, in.Id).Count()
if e != nil {
return e
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.MobileCipher] = cipher
data[mc.MobileHash] = hash
data[mc.MobileMask] = encrypt.MaskMobile(in.Mobile)
data[mc.Mobile] = in.Mobile
}
if in.Password != "" {
hashed, e := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
@@ -206,7 +192,7 @@ func (s *sSysAppMember) Edit(ctx context.Context, in *sysin.AppMemberEditInp) er
data[mc.InviteCode] = strings.ToUpper(grand.S(8))
data[mc.MemberStatus] = consts.AppMemberStatusNormal
data[mc.CreatedBy] = userId
result, e := s.memberModel(ctx).Fields(mc.MemberNo, mc.Username, mc.MobileCipher, mc.MobileHash, mc.MobileMask, mc.PasswordHash, mc.TradePasswordHash, mc.LevelId, mc.ParentId, mc.Level, mc.Tree, mc.InviteCode, mc.MemberStatus, mc.RiskStatus, mc.AutoDispatch, mc.CreditScore, mc.IsTestAccount, mc.CreatedBy, mc.UpdatedBy).Hook(hook.SaveTenant).Data(data).Insert()
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, "创建会员失败")
}
@@ -442,7 +428,7 @@ func (s *sSysAppMember) Sensitive(ctx context.Context, in *sysin.AppMemberSensit
case "member":
var member *entity.AppMember
if err = s.memberModel(ctx).WherePri(in.TargetId).Scan(&member); err == nil && member != nil {
res.Fields["mobile"], err = encrypt.DecryptField(ctx, member.MobileCipher)
res.Fields["mobile"] = member.Mobile
var p *entity.AppMemberProfile
_ = s.profileModel(ctx).Where(dao.AppMemberProfile.Columns().MemberId, in.TargetId).Scan(&p)
if p != nil {
@@ -494,7 +480,7 @@ func (s *sSysAppMember) Team(ctx context.Context, in *sysin.AppMemberTeamInp) (l
mc, wc := dao.AppMember.Columns(), dao.WalletAccount.Columns()
token := "tr_" + fmt.Sprint(in.MemberId) + " "
maxAbsolute := root.Level + in.MaxLevel
mod := s.memberModel(ctx).As("m").LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("m", mc.Id)+" AND "+col("w", wc.Currency)+"='CNY'").Fields(col("m", mc.Id), col("m", mc.Username), col("m", mc.MobileMask), col("m", mc.ParentId), fmt.Sprintf("%s-%d relative_level", col("m", mc.Level), root.Level), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), col("m", mc.MemberStatus), col("m", mc.CreatedAt)).WhereLike(col("m", mc.Tree), "%"+token+"%").WhereLTE(col("m", mc.Level), maxAbsolute)
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, "查询会员团队失败")
@@ -569,7 +555,7 @@ func (s *sSysAppMember) StatusExecute(ctx context.Context, in *sysin.AppMemberSt
func (s *sSysAppMember) WalletList(ctx context.Context, in *sysin.WalletAdjustmentListInp) (list []*sysin.WalletAdjustmentListModel, total int, err error) {
ac, mc := dao.WalletAdjustmentRequest.Columns(), dao.AppMember.Columns()
mod := s.adjustmentModel(ctx).As("a").LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("a", ac.MemberId)).Fields(col("a", ac.Id), col("a", ac.RequestNo), col("a", ac.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("a", ac.AdjustType), col("a", ac.Direction), col("a", ac.Amount), col("a", ac.ReasonCode), col("a", ac.ReasonText), col("a", ac.RequestStatus), col("a", ac.ApprovalRequestId), col("a", ac.LedgerId), col("a", ac.CreatedAt))
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)
}
+2 -7
View File
@@ -12,7 +12,6 @@ import (
"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"
@@ -42,7 +41,7 @@ func (s *sSysRechargeOrder) baseModel(ctx context.Context) *gdb.Model {
func (s *sSysRechargeOrder) listModel(ctx context.Context) *gdb.Model {
rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns()
return s.baseModel(ctx).
Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt))
Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.Mobile), col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt))
}
func (s *sSysRechargeOrder) filter(ctx context.Context, mod *gdb.Model, in *sysin.RechargeOrderListInp) (*gdb.Model, error) {
@@ -54,11 +53,7 @@ func (s *sSysRechargeOrder) filter(ctx context.Context, mod *gdb.Model, in *sysi
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, err := encrypt.HashField(ctx, in.Mobile)
if err != nil {
return nil, err
}
mod = mod.Where(col("m", mc.MobileHash), hash)
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
}
if in.RechargeStatus > 0 {
mod = mod.Where(col("r", rc.RechargeStatus), in.RechargeStatus)
+5 -7
View File
@@ -12,7 +12,6 @@ import (
"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"
@@ -30,19 +29,18 @@ func (s *sSysTaskSubmission) Model(ctx context.Context) *gdb.Model {
}
func (s *sSysTaskSubmission) listModel(ctx context.Context) *gdb.Model {
sc, tc, mc, lc := dao.TaskSubmission.Columns(), dao.Task.Columns(), dao.AppMember.Columns(), dao.AppMemberLevel.Columns()
return s.Model(ctx).As("s").LeftJoin(dao.Task.Table()+" t", col("t", tc.Id)+"="+col("s", sc.TaskId)).LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("s", sc.MemberId)).LeftJoin(dao.AppMemberLevel.Table()+" l", col("l", lc.Id)+"="+col("s", sc.MemberLevelId)).Fields(col("s", sc.Id), col("s", sc.SubmissionNo), col("s", sc.TradeOrderId), col("s", sc.TaskId), fmt.Sprintf("COALESCE(%s,'') task_title", col("t", tc.Title)), col("s", sc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), fmt.Sprintf("COALESCE(%s,'') level_name", col("l", lc.Name)), col("s", sc.EvidenceText), col("s", sc.EvidenceAttachmentIds), col("s", sc.SubmissionStatus), col("s", sc.CommissionAmount), col("s", sc.SuccessRate), col("s", sc.ReviewReasonCode), col("s", sc.ReviewReason), col("s", sc.AllowResubmit), col("s", sc.LedgerId), col("s", sc.ReviewedAt), col("s", sc.SettledAt), col("s", sc.CreatedAt))
return s.Model(ctx).As("s").LeftJoin(dao.Task.Table()+" t", col("t", tc.Id)+"="+col("s", sc.TaskId)).LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("s", sc.MemberId)).LeftJoin(dao.AppMemberLevel.Table()+" l", col("l", lc.Id)+"="+col("s", sc.MemberLevelId)).Fields(col("s", sc.Id), col("s", sc.SubmissionNo), col("s", sc.TradeOrderId), col("s", sc.TaskId), fmt.Sprintf("COALESCE(%s,'') task_title", col("t", tc.Title)), col("s", sc.MemberId), col("m", mc.Username), col("m", mc.Mobile), fmt.Sprintf("COALESCE(%s,'') level_name", col("l", lc.Name)), col("s", sc.EvidenceText), col("s", sc.EvidenceAttachmentIds), col("s", sc.SubmissionStatus), col("s", sc.CommissionAmount), col("s", sc.SuccessRate), col("s", sc.ReviewReasonCode), col("s", sc.ReviewReason), col("s", sc.AllowResubmit), col("s", sc.LedgerId), col("s", sc.ReviewedAt), col("s", sc.SettledAt), col("s", sc.CreatedAt))
}
func (s *sSysTaskSubmission) filter(ctx context.Context, mod *gdb.Model, in *sysin.TaskSubmissionListInp) (*gdb.Model, error) {
sc, mc := dao.TaskSubmission.Columns(), dao.AppMember.Columns()
if in.TradeOrderId > 0 {
mod = mod.Where(col("s", sc.TradeOrderId), in.TradeOrderId)
}
if in.Username != "" {
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, err := encrypt.HashField(ctx, in.Mobile)
if err != nil {
return nil, err
}
mod = mod.Where(col("m", mc.MobileHash), hash)
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
}
if in.SubmissionStatus > 0 {
mod = mod.Where(col("s", sc.SubmissionStatus), in.SubmissionStatus)
+60 -10
View File
@@ -9,10 +9,10 @@ import (
"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"
@@ -30,14 +30,15 @@ func (s *sSysTradeOrder) model(ctx context.Context) *gdb.Model {
}
func (s *sSysTradeOrder) listModel(ctx context.Context) *gdb.Model {
oc, mc, pc, wc, ac := dao.TradeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.ApprovalRequest.Columns()
oc, mc, pc, wc, ac, sc := dao.TradeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.ApprovalRequest.Columns(), dao.TaskSubmission.Columns()
todayCount := fmt.Sprintf("(SELECT COUNT(*) FROM %s td WHERE td.%s=%s AND td.%s>=CURDATE()) today_trade_count", dao.TradeOrder.Table(), oc.MemberId, col("o", oc.MemberId), oc.CreatedAt)
canReview := fmt.Sprintf("EXISTS(SELECT 1 FROM %s ts WHERE ts.%s=%s AND ts.%s=%d) can_review", dao.TaskSubmission.Table(), sc.TradeOrderId, col("o", oc.Id), sc.SubmissionStatus, consts.TaskSubmissionPending)
return s.model(ctx).As("o").
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("o", oc.MemberId)).
LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("o", oc.MemberId)).
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("o", oc.MemberId)+" AND "+col("w", wc.Currency)+"='CNY'").
LeftJoin(dao.ApprovalRequest.Table()+" a", col("a", ac.Id)+"="+col("o", oc.ApprovalRequestId)).
Fields(col("o", oc.Id), col("o", oc.OrderNo), col("o", oc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), col("m", mc.AutoDispatch), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), todayCount, col("o", oc.ProductName), col("o", oc.OrderType), col("o", oc.Quantity), col("o", oc.UnitPriceAmount), col("o", oc.TotalAmount), col("o", oc.CommissionAmount), col("o", oc.Currency), col("o", oc.Message), col("o", oc.OrderStatus), col("o", oc.FreezeStatus), col("o", oc.ApprovalRequestId), fmt.Sprintf("COALESCE(%s,0) approval_status", col("a", ac.ApprovalStatus)), col("o", oc.MatchedAt), col("o", oc.CompletedAt), col("o", oc.UnfrozenAt), col("o", oc.CreatedAt))
Fields(col("o", oc.Id), col("o", oc.OrderNo), col("o", oc.MemberId), col("m", mc.Username), col("m", mc.Mobile), col("p", pc.RealNameMask), col("m", mc.AutoDispatch), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), todayCount, canReview, col("o", oc.ProductName), col("o", oc.OrderType), col("o", oc.Quantity), col("o", oc.UnitPriceAmount), col("o", oc.TotalAmount), col("o", oc.CommissionAmount), col("o", oc.Currency), col("o", oc.Message), col("o", oc.OrderStatus), col("o", oc.FreezeStatus), col("o", oc.ApprovalRequestId), fmt.Sprintf("COALESCE(%s,0) approval_status", col("a", ac.ApprovalStatus)), col("o", oc.MatchedAt), col("o", oc.CompletedAt), col("o", oc.UnfrozenAt), col("o", oc.CreatedAt))
}
func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp) (list []*sysin.TradeOrderListModel, total int, err error) {
@@ -50,11 +51,7 @@ func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp)
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, e := encrypt.HashField(ctx, in.Mobile)
if e != nil {
return nil, 0, e
}
mod = mod.Where(col("m", mc.MobileHash), hash)
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
}
if in.OrderStatus > 0 {
mod = mod.Where(col("o", oc.OrderStatus), in.OrderStatus)
@@ -97,6 +94,7 @@ func (s *sSysTradeOrder) MatchProducts(ctx context.Context, in *sysin.TradeOrder
mod := handler.Model(dao.Product.Ctx(ctx), &handler.Option{FilterTenant: true}).
Fields(pc.Id, pc.Name, pc.CoverUrl, pc.PriceAmount).
Where(pc.PublishStatus, consts.BizPublishPublished).
Where(pc.Currency, "CNY").
WhereNull(pc.DeletedAt).
Where("("+pc.StartAt+" IS NULL OR "+pc.StartAt+" <= ?)", gtime.Now()).
Where("("+pc.EndAt+" IS NULL OR "+pc.EndAt+" >= ?)", gtime.Now())
@@ -153,6 +151,9 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
if product == nil {
return gerror.New("商品不存在、已下架或无权使用")
}
if product.Currency != "CNY" {
return gerror.New("任务订单只能匹配 CNY 商品")
}
now := gtime.Now()
if product.StartAt != nil && product.StartAt.After(now) || product.EndAt != nil && product.EndAt.Before(now) {
return gerror.New("商品当前不在可匹配时间范围内")
@@ -186,7 +187,38 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
return gerror.New("会员等级生效版本不存在")
}
totalAmount := product.PriceAmount * int64(in.Quantity)
wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount
if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).
Where(wc.MemberId, order.MemberId).Where(wc.Currency, product.Currency).LockUpdate().Scan(&account); err != nil {
return gerror.Wrap(err, "读取会员钱包失败")
}
if account == nil || account.AccountStatus != 1 {
return gerror.New("会员钱包不存在或状态异常")
}
if totalAmount > account.AvailableAmount {
return gerror.New("匹配商品总额不能超过会员可用余额")
}
commissionAmount := totalAmount * int64(levelVersion.CommissionRate) / 10000
afterAvailable := account.AvailableAmount - totalAmount
afterFrozen := account.FrozenAmount + totalAmount
walletResult, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).
WherePri(account.Id).Where(wc.Version, account.Version).
Data(g.Map{wc.AvailableAmount: afterAvailable, wc.FrozenAmount: afterFrozen, wc.Version: gdb.Raw(wc.Version + "+1")}).Update()
if err != nil {
return gerror.Wrap(err, "扣除订单本金失败")
}
if affected, _ := walletResult.RowsAffected(); affected != 1 {
return gerror.New("会员余额已变化,请刷新后重试")
}
if err = insertTradeMatchLedger(ctx, order, account, fmt.Sprintf("WL%dMA", order.Id), "available", consts.WalletDirectionExpense,
totalAmount, account.AvailableAmount, afterAvailable, "match:available", "人工匹配扣除可用余额"); err != nil {
return err
}
if err = insertTradeMatchLedger(ctx, order, account, fmt.Sprintf("WL%dMF", order.Id), "frozen", consts.WalletDirectionIncome,
totalAmount, account.FrozenAmount, afterFrozen, "match:frozen", "人工匹配冻结订单本金"); err != nil {
return err
}
snapshot := gjson.New(g.Map{
"source": "admin_manual_match", "productId": product.Id, "productNo": product.ProductNo,
"unitPriceAmount": product.PriceAmount, "quantity": in.Quantity,
@@ -195,10 +227,10 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
})
oc := dao.TradeOrder.Columns()
data := g.Map{
oc.ProductId: product.Id, oc.ProductName: product.Name, oc.OrderType: consts.TradeOrderTypeManual,
oc.ProductId: product.Id, oc.ProductName: product.Name,
oc.Quantity: in.Quantity, oc.UnitPriceAmount: product.PriceAmount, oc.TotalAmount: totalAmount,
oc.CommissionAmount: commissionAmount, oc.Currency: product.Currency, oc.MemberLevelId: levelId,
oc.RuleSnapshot: snapshot, oc.Message: "后台人工匹配商品", oc.OrderStatus: consts.TradeOrderPending,
oc.RuleSnapshot: snapshot, oc.Message: "后台人工匹配商品,订单本金已扣除", oc.OrderStatus: consts.TradeOrderPending,
oc.FreezeStatus: consts.TradeFreezeNormal, oc.MatchedAt: now, oc.UpdatedBy: contexts.GetUserId(ctx),
}
result, err := s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderMatching).Data(data).Update()
@@ -217,6 +249,24 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp
})
}
func insertTradeMatchLedger(ctx context.Context, order *entity.TradeOrder, account *entity.WalletAccount,
ledgerNo, entryType string, direction int, amount, beforeAmount, afterAmount int64, suffix, remark string) error {
lc := dao.WalletLedger.Columns()
_, 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: order.MemberId,
lc.BizType: "task_principal_hold", lc.BizNo: order.OrderNo, lc.EntryType: entryType, lc.Direction: direction,
lc.Amount: amount, lc.BeforeAmount: beforeAmount, lc.AfterAmount: afterAmount, lc.Currency: account.Currency,
lc.IdempotencyKey: fmt.Sprintf("task:%d:%s", order.Id, suffix), lc.Remark: remark,
lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: gtime.Now()}).Insert()
if err != nil {
return gerror.Wrap(err, "记录订单扣款流水失败")
}
return nil
}
func (s *sSysTradeOrder) Apply(ctx context.Context, in *sysin.TradeOrderApplyInp) (res *sysin.FinanceApprovalModel, err error) {
var order *entity.TradeOrder
if err = s.model(ctx).WherePri(in.Id).Scan(&order); err != nil {
@@ -12,7 +12,6 @@ import (
"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"
@@ -38,7 +37,7 @@ func (s *sSysWithdrawalOrder) listModel(ctx context.Context) *gdb.Model {
LeftJoin(dao.WalletAccount.Table()+" w", col("w", ac.MemberId)+"="+col("o", wc.MemberId)+" AND "+col("w", ac.Currency)+"='CNY'").
LeftJoin(dao.AppMemberPaymentAccount.Table()+" pa", col("pa", pc.Id)+"="+col("o", wc.PaymentAccountId)).
LeftJoin(dao.SysAttachment.Table()+" sa", col("sa", sc.Id)+"="+col("pa", pc.QrAttachmentId)).
Fields(col("o", wc.Id), col("o", wc.WithdrawalNo), col("o", wc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", ac.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", ac.FrozenAmount)), col("o", wc.RequestAmount), col("o", wc.FeeAmount), col("o", wc.ActualAmount), col("o", wc.Currency), col("o", wc.AccountMask), col("o", wc.BankName), col("o", wc.HolderMask), fmt.Sprintf("COALESCE(%s,0) payment_account_type", col("pa", pc.AccountType)), fmt.Sprintf("COALESCE(%s,'') qr_code_url", col("sa", sc.FileUrl)), col("o", wc.RiskResult), col("o", wc.WithdrawalStatus), col("o", wc.ReviewReasonCode), col("o", wc.ReviewReason), col("o", wc.ApprovalRequestId), col("o", wc.ExternalNo), col("o", wc.FreezeLedgerId), col("o", wc.FinalLedgerId), col("o", wc.ReviewedAt), col("o", wc.ProcessedAt), col("o", wc.CompletedAt), col("o", wc.CreatedAt))
Fields(col("o", wc.Id), col("o", wc.WithdrawalNo), col("o", wc.MemberId), col("m", mc.Username), col("m", mc.Mobile), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", ac.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", ac.FrozenAmount)), col("o", wc.RequestAmount), col("o", wc.FeeAmount), col("o", wc.ActualAmount), col("o", wc.Currency), col("o", wc.AccountMask), col("o", wc.BankName), col("o", wc.HolderMask), fmt.Sprintf("COALESCE(%s,0) payment_account_type", col("pa", pc.AccountType)), fmt.Sprintf("COALESCE(%s,'') qr_code_url", col("sa", sc.FileUrl)), col("o", wc.RiskResult), col("o", wc.WithdrawalStatus), col("o", wc.ReviewReasonCode), col("o", wc.ReviewReason), col("o", wc.ApprovalRequestId), col("o", wc.ExternalNo), col("o", wc.FreezeLedgerId), col("o", wc.FinalLedgerId), col("o", wc.ReviewedAt), col("o", wc.ProcessedAt), col("o", wc.CompletedAt), col("o", wc.CreatedAt))
}
func (s *sSysWithdrawalOrder) List(ctx context.Context, in *sysin.WithdrawalOrderListInp) (list []*sysin.WithdrawalOrderListModel, total int, err error) {
@@ -51,11 +50,7 @@ func (s *sSysWithdrawalOrder) List(ctx context.Context, in *sysin.WithdrawalOrde
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, e := encrypt.HashField(ctx, in.Mobile)
if e != nil {
return nil, 0, e
}
mod = mod.Where(col("m", mc.MobileHash), hash)
mod = mod.Where(col("m", mc.Mobile), in.Mobile)
}
if in.WithdrawalStatus > 0 {
mod = mod.Where(col("o", wc.WithdrawalStatus), in.WithdrawalStatus)
+1 -3
View File
@@ -16,9 +16,7 @@ type AppMember struct {
TenantId any // 租户ID
MemberNo any // 会员编号
Username any // 用户名
MobileCipher any // 手机号密文(AES-GCM)
MobileHash any // 手机号检索哈希(HMAC-SHA256)
MobileMask any // 手机号脱敏值
Mobile any //
PasswordHash any // 登录密码哈希(bcrypt)
TradePasswordHash any // 交易密码哈希
LevelId any // 会员等级ID
+1 -3
View File
@@ -14,9 +14,7 @@ type AppMember struct {
TenantId uint64 `json:"tenantId" orm:"tenant_id" description:"租户ID"`
MemberNo string `json:"memberNo" orm:"member_no" description:"会员编号"`
Username string `json:"username" orm:"username" description:"用户名"`
MobileCipher string `json:"mobileCipher" orm:"mobile_cipher" description:"手机号密文(AES-GCM)"`
MobileHash string `json:"mobileHash" orm:"mobile_hash" description:"手机号检索哈希(HMAC-SHA256)"`
MobileMask string `json:"mobileMask" orm:"mobile_mask" description:"手机号脱敏值"`
Mobile string `json:"mobile" orm:"mobile" description:""`
PasswordHash string `json:"passwordHash" orm:"password_hash" description:"登录密码哈希(bcrypt)"`
TradePasswordHash string `json:"tradePasswordHash" orm:"trade_password_hash" description:"交易密码哈希"`
LevelId uint64 `json:"levelId" orm:"level_id" description:"会员等级ID"`
@@ -30,7 +30,7 @@ type AppMemberListModel struct {
Id uint64 `json:"id"`
MemberNo string `json:"memberNo"`
Username string `json:"username"`
MobileMask string `json:"mobileMask"`
Mobile string `json:"mobile"`
RealNameMask string `json:"realNameMask"`
AvailableAmount int64 `json:"availableAmount"`
FrozenAmount int64 `json:"frozenAmount"`
@@ -79,7 +79,7 @@ type AppMemberEditInp struct {
LevelId uint64 `json:"levelId"`
RiskStatus int `json:"riskStatus" d:"1"`
CreditScore int `json:"creditScore" d:"100" v:"between:0,1000#信用分范围为0到1000"`
AutoDispatch int `json:"autoDispatch" d:"2"`
AutoDispatch int `json:"autoDispatch" d:"1"`
MemberCategory int `json:"memberCategory"`
IsTestAccount int `json:"isTestAccount"`
}
@@ -280,7 +280,7 @@ func (in *AppMemberTeamInp) Filter(ctx context.Context) error { return nil }
type AppMemberTeamModel struct {
Id uint64 `json:"id"`
Username string `json:"username"`
MobileMask string `json:"mobileMask"`
Mobile string `json:"mobile"`
ParentId uint64 `json:"parentId"`
RelativeLevel int `json:"relativeLevel"`
TeamCount int `json:"teamCount"`
@@ -352,7 +352,7 @@ type WalletAdjustmentListModel struct {
RequestNo string `json:"requestNo"`
MemberId uint64 `json:"memberId"`
Username string `json:"username"`
MobileMask string `json:"mobileMask"`
Mobile string `json:"mobile"`
AdjustType int `json:"adjustType"`
Direction int `json:"direction"`
Amount int64 `json:"amount"`
@@ -218,6 +218,7 @@ func (in *TaskDeleteInp) Filter(ctx context.Context) error { return nil }
type TaskSubmissionListInp struct {
form.PageReq
TradeOrderId uint64 `json:"tradeOrderId"`
Username string `json:"username"`
Mobile string `json:"mobile"`
SubmissionStatus int `json:"submissionStatus"`
@@ -234,7 +235,7 @@ type TaskSubmissionListModel struct {
TaskTitle string `json:"taskTitle"`
MemberId uint64 `json:"memberId"`
Username string `json:"username"`
MobileMask string `json:"mobileMask"`
Mobile string `json:"mobile"`
LevelName string `json:"levelName"`
EvidenceText string `json:"evidenceText"`
EvidenceAttachmentIds *gjson.Json `json:"evidenceAttachmentIds"`
+4 -3
View File
@@ -30,7 +30,7 @@ type TradeOrderListModel struct {
OrderNo string `json:"orderNo"`
MemberId uint64 `json:"memberId"`
Username string `json:"username"`
MobileMask string `json:"mobileMask"`
Mobile string `json:"mobile"`
RealNameMask string `json:"realNameMask"`
AutoDispatch int `json:"autoDispatch"`
AvailableAmount int64 `json:"availableAmount"`
@@ -53,6 +53,7 @@ type TradeOrderListModel struct {
CreatedAt *gtime.Time `json:"createdAt"`
ApprovalStatus int `json:"approvalStatus"`
CanMatch bool `json:"canMatch"`
CanReview bool `json:"canReview"`
CanApply bool `json:"canApply"`
CanExecute bool `json:"canExecute"`
}
@@ -133,7 +134,7 @@ type RechargeOrderListModel struct {
RechargeNo string `json:"rechargeNo"`
MemberId uint64 `json:"memberId"`
Username string `json:"username"`
MobileMask string `json:"mobileMask"`
Mobile string `json:"mobile"`
RealNameMask string `json:"realNameMask"`
AvailableAmount int64 `json:"availableAmount"`
ChannelId uint64 `json:"channelId"`
@@ -213,7 +214,7 @@ type WithdrawalOrderListModel struct {
WithdrawalNo string `json:"withdrawalNo"`
MemberId uint64 `json:"memberId"`
Username string `json:"username"`
MobileMask string `json:"mobileMask"`
Mobile string `json:"mobile"`
AvailableAmount int64 `json:"availableAmount"`
FrozenAmount int64 `json:"frozenAmount"`
RequestAmount int64 `json:"requestAmount"`