package sys import ( "context" "fmt" "math" "hotgo/internal/consts" "hotgo/internal/dao" "hotgo/internal/library/contexts" "hotgo/internal/library/hgorm/handler" "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" "hotgo/utility/encrypt" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) type sSysTradeOrder struct{} func NewSysTradeOrder() *sSysTradeOrder { return &sSysTradeOrder{} } func init() { service.RegisterSysTradeOrder(NewSysTradeOrder()) } func (s *sSysTradeOrder) model(ctx context.Context) *gdb.Model { return handler.Model(dao.TradeOrder.Ctx(ctx), &handler.Option{FilterTenant: true}) } func (s *sSysTradeOrder) listModel(ctx context.Context) *gdb.Model { oc, mc, pc, wc, ac := dao.TradeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.ApprovalRequest.Columns() todayCount := fmt.Sprintf("(SELECT COUNT(*) FROM %s td WHERE td.%s=%s AND td.%s>=CURDATE()) today_trade_count", dao.TradeOrder.Table(), oc.MemberId, col("o", oc.MemberId), oc.CreatedAt) return s.model(ctx).As("o"). LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("o", oc.MemberId)). LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("o", oc.MemberId)). LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("o", oc.MemberId)+" AND "+col("w", wc.Currency)+"='CNY'"). LeftJoin(dao.ApprovalRequest.Table()+" a", col("a", ac.Id)+"="+col("o", oc.ApprovalRequestId)). Fields(col("o", oc.Id), col("o", oc.OrderNo), col("o", oc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), col("m", mc.AutoDispatch), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), fmt.Sprintf("COALESCE(%s,0) frozen_amount", col("w", wc.FrozenAmount)), todayCount, col("o", oc.ProductName), col("o", oc.OrderType), col("o", oc.Quantity), col("o", oc.UnitPriceAmount), col("o", oc.TotalAmount), col("o", oc.CommissionAmount), col("o", oc.Currency), col("o", oc.Message), col("o", oc.OrderStatus), col("o", oc.FreezeStatus), col("o", oc.ApprovalRequestId), fmt.Sprintf("COALESCE(%s,0) approval_status", col("a", ac.ApprovalStatus)), col("o", oc.MatchedAt), col("o", oc.CompletedAt), col("o", oc.UnfrozenAt), col("o", oc.CreatedAt)) } func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp) (list []*sysin.TradeOrderListModel, total int, err error) { oc, mc := dao.TradeOrder.Columns(), dao.AppMember.Columns() mod := s.listModel(ctx) if in.OrderNo != "" { mod = mod.WhereLike(col("o", oc.OrderNo), "%"+in.OrderNo+"%") } if in.Username != "" { mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%") } if in.Mobile != "" { hash, e := encrypt.HashField(ctx, in.Mobile) if e != nil { return nil, 0, e } mod = mod.Where(col("m", mc.MobileHash), hash) } if in.OrderStatus > 0 { mod = mod.Where(col("o", oc.OrderStatus), in.OrderStatus) } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween(col("o", oc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1]) } matchFirst := fmt.Sprintf("CASE WHEN %s=%d THEN 0 ELSE 1 END", col("o", oc.OrderStatus), consts.TradeOrderMatching) err = mod.Page(in.Page, in.PerPage).OrderAsc(matchFirst).OrderDesc(col("o", oc.Id)).ScanAndCount(&list, &total, false) if err != nil { err = gerror.Wrap(err, "获取交易订单失败") return } for _, row := range list { row.CanMatch = row.OrderStatus == consts.TradeOrderMatching && row.ApprovalRequestId == 0 row.CanApply = row.ApprovalRequestId == 0 && row.OrderStatus != consts.TradeOrderCompleted && row.OrderStatus != consts.TradeOrderCancelled row.CanExecute = row.ApprovalRequestId > 0 && row.ApprovalStatus == consts.ApprovalStatusApproved } return } func (s *sSysTradeOrder) View(ctx context.Context, in *sysin.TradeOrderViewInp) (res *sysin.TradeOrderViewModel, err error) { var base *sysin.TradeOrderListModel if err = s.listModel(ctx).Where(col("o", dao.TradeOrder.Columns().Id), in.Id).Scan(&base); err != nil { return nil, gerror.Wrap(err, "获取订单详情失败") } if base == nil { return nil, gerror.New("交易订单不存在") } var order *entity.TradeOrder if err = s.model(ctx).WherePri(in.Id).Scan(&order); err != nil { return nil, err } res = &sysin.TradeOrderViewModel{TradeOrderListModel: *base, TaskId: order.TaskId, ProductId: order.ProductId, MemberLevelId: order.MemberLevelId, RuleSnapshot: order.RuleSnapshot} return } func (s *sSysTradeOrder) MatchProducts(ctx context.Context, in *sysin.TradeOrderMatchProductListInp) (list []*sysin.TradeOrderMatchProductModel, total int, err error) { pc := dao.Product.Columns() mod := handler.Model(dao.Product.Ctx(ctx), &handler.Option{FilterTenant: true}). Fields(pc.Id, pc.Name, pc.CoverUrl, pc.PriceAmount). Where(pc.PublishStatus, consts.BizPublishPublished). WhereNull(pc.DeletedAt). Where("("+pc.StartAt+" IS NULL OR "+pc.StartAt+" <= ?)", gtime.Now()). Where("("+pc.EndAt+" IS NULL OR "+pc.EndAt+" >= ?)", gtime.Now()) if in.Name != "" { mod = mod.WhereLike(pc.Name, "%"+in.Name+"%") } if in.PriceMin != "" { amount, parseErr := parseCents(in.PriceMin) if parseErr != nil { return nil, 0, gerror.New("最低价格格式不正确") } mod = mod.WhereGTE(pc.PriceAmount, amount) } if in.PriceMax != "" { amount, parseErr := parseCents(in.PriceMax) if parseErr != nil { return nil, 0, gerror.New("最高价格格式不正确") } mod = mod.WhereLTE(pc.PriceAmount, amount) } if in.PriceMin != "" && in.PriceMax != "" { minAmount, _ := parseCents(in.PriceMin) maxAmount, _ := parseCents(in.PriceMax) if minAmount > maxAmount { return nil, 0, gerror.New("最低价格不能高于最高价格") } } err = mod.Page(in.Page, in.PerPage).OrderDesc(pc.PriceAmount).OrderDesc(pc.Id).ScanAndCount(&list, &total, false) if err != nil { err = gerror.Wrap(err, "获取可匹配商品失败") } return } func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp) error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var order *entity.TradeOrder if err := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&order); err != nil { return gerror.Wrap(err, "读取待匹配订单失败") } if order == nil { return gerror.New("交易订单不存在") } if order.OrderStatus != consts.TradeOrderMatching || order.ApprovalRequestId > 0 { return gerror.New("当前订单状态不允许匹配商品") } var product *entity.Product pc := dao.Product.Columns() if err := handler.Model(dao.Product.Ctx(ctx), &handler.Option{FilterTenant: true}). WherePri(in.ProductId).Where(pc.PublishStatus, consts.BizPublishPublished).WhereNull(pc.DeletedAt). LockUpdate().Scan(&product); err != nil { return gerror.Wrap(err, "读取匹配商品失败") } if product == nil { return gerror.New("商品不存在、已下架或无权使用") } now := gtime.Now() if product.StartAt != nil && product.StartAt.After(now) || product.EndAt != nil && product.EndAt.Before(now) { return gerror.New("商品当前不在可匹配时间范围内") } if product.PriceAmount <= 0 || product.PriceAmount > math.MaxInt64/int64(in.Quantity) { return gerror.New("商品价格或数量不合法") } levelId := order.MemberLevelId if levelId == 0 { var member *entity.AppMember if err := handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).Fields(dao.AppMember.Columns().LevelId).WherePri(order.MemberId).Scan(&member); err != nil { return gerror.Wrap(err, "读取会员等级失败") } if member == nil { return gerror.New("订单会员不存在") } levelId = member.LevelId } var level *entity.AppMemberLevel if err := handler.Model(dao.AppMemberLevel.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(levelId).Scan(&level); err != nil { return gerror.Wrap(err, "读取会员等级规则失败") } if level == nil || level.CurrentVersionId == 0 { return gerror.New("会员等级尚未配置生效规则") } var levelVersion *entity.AppMemberLevelVersion if err := handler.Model(dao.AppMemberLevelVersion.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(level.CurrentVersionId).Scan(&levelVersion); err != nil { return gerror.Wrap(err, "读取会员等级版本失败") } if levelVersion == nil { return gerror.New("会员等级生效版本不存在") } totalAmount := product.PriceAmount * int64(in.Quantity) commissionAmount := totalAmount * int64(levelVersion.CommissionRate) / 10000 snapshot := gjson.New(g.Map{ "source": "admin_manual_match", "productId": product.Id, "productNo": product.ProductNo, "unitPriceAmount": product.PriceAmount, "quantity": in.Quantity, "memberLevelId": levelId, "levelVersionId": levelVersion.Id, "commissionRate": levelVersion.CommissionRate, }) oc := dao.TradeOrder.Columns() data := g.Map{ oc.ProductId: product.Id, oc.ProductName: product.Name, oc.OrderType: consts.TradeOrderTypeManual, oc.Quantity: in.Quantity, oc.UnitPriceAmount: product.PriceAmount, oc.TotalAmount: totalAmount, oc.CommissionAmount: commissionAmount, oc.Currency: product.Currency, oc.MemberLevelId: levelId, oc.RuleSnapshot: snapshot, oc.Message: "后台人工匹配商品", oc.OrderStatus: consts.TradeOrderPending, oc.FreezeStatus: consts.TradeFreezeNormal, oc.MatchedAt: now, oc.UpdatedBy: contexts.GetUserId(ctx), } result, err := s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderMatching).Data(data).Update() if err != nil { return gerror.Wrap(err, "匹配订单失败") } rows, _ := result.RowsAffected() if rows != 1 { return gerror.New("订单状态已变化,请刷新后重试") } return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{ Module: "trade", Action: "trade_order.match", TargetType: "trade_order", TargetId: order.Id, Before: order, After: data, ChangeJson: gjson.New(g.Map{"productId": product.Id, "quantity": in.Quantity, "totalAmount": totalAmount}), Reason: "后台人工匹配商品", }) }) } func (s *sSysTradeOrder) Apply(ctx context.Context, in *sysin.TradeOrderApplyInp) (res *sysin.FinanceApprovalModel, err error) { var order *entity.TradeOrder if err = s.model(ctx).WherePri(in.Id).Scan(&order); err != nil { return } if order == nil { return nil, gerror.New("交易订单不存在") } if order.ApprovalRequestId > 0 { return nil, gerror.New("该订单已有待执行处理申请") } switch in.Action { case consts.TradeActionCancelMatch: if order.OrderStatus != consts.TradeOrderMatching { return nil, gerror.New("仅匹配中订单可以取消匹配") } case consts.TradeActionRematch: if order.OrderStatus != consts.TradeOrderTimeout && order.OrderStatus != consts.TradeOrderFailed { return nil, gerror.New("仅超时或失败订单可以重新匹配") } case consts.TradeActionUnfreeze: if order.OrderStatus != consts.TradeOrderFrozen && order.FreezeStatus != consts.TradeFreezeFrozen { return nil, gerror.New("订单当前未冻结") } } payload := g.Map{"id": order.Id, "action": in.Action, "reasonCode": in.ReasonCode, "reasonText": in.ReasonText} approval, err := service.SysApproval().Create(ctx, &sysin.ApprovalCreateInp{ApprovalType: "trade_order_action", TargetType: "trade_order", TargetId: order.Id, Action: in.Action, Title: "交易订单人工处理 " + order.OrderNo, PayloadJson: gjson.New(payload), RiskLevel: consts.ApprovalRiskHigh, RequiredApprovals: 1, Remark: in.ReasonText}) if err != nil { return nil, err } if err = service.SysApproval().Submit(ctx, &sysin.ApprovalSubmitInp{Id: approval.Id}); err != nil { return nil, err } data := g.Map{dao.TradeOrder.Columns().ApprovalRequestId: approval.Id, dao.TradeOrder.Columns().UpdatedBy: contexts.GetUserId(ctx)} if in.Action == consts.TradeActionUnfreeze { data[dao.TradeOrder.Columns().FreezeStatus] = consts.TradeFreezePending } if _, err = s.model(ctx).WherePri(order.Id).Data(data).Update(); err != nil { return nil, gerror.Wrap(err, "登记订单处理申请失败") } return &sysin.FinanceApprovalModel{ApprovalId: approval.Id, ApprovalNo: approval.ApprovalNo}, nil } func (s *sSysTradeOrder) Execute(ctx context.Context, in *sysin.TradeOrderExecuteInp) error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var approval *entity.ApprovalRequest if err := service.SysApproval().Model(ctx).WherePri(in.ApprovalId).LockUpdate().Scan(&approval); err != nil { return err } if approval == nil || approval.ApprovalType != "trade_order_action" { return gerror.New("订单处理审批不存在") } if approval.ApprovalStatus != consts.ApprovalStatusApproved || approval.ExecutedAt != nil { return gerror.New("审批尚未通过或已执行") } payload := approval.PayloadJson.Map() orderId := gjson.New(payload).Get("id").Uint64() action := gjson.New(payload).Get("action").String() reason := gjson.New(payload).Get("reasonText").String() var order *entity.TradeOrder if err := s.model(ctx).WherePri(orderId).LockUpdate().Scan(&order); err != nil { return err } if order == nil || order.ApprovalRequestId != approval.Id { return gerror.New("订单处理申请已失效") } oc := dao.TradeOrder.Columns() data := g.Map{oc.ApprovalRequestId: 0, oc.UpdatedBy: contexts.GetUserId(ctx)} switch action { case consts.TradeActionCancelMatch: if order.OrderStatus != consts.TradeOrderMatching { return gerror.New("订单状态已变化,不能取消匹配") } data[oc.OrderStatus] = consts.TradeOrderCancelled case consts.TradeActionRematch: if order.OrderStatus != consts.TradeOrderTimeout && order.OrderStatus != consts.TradeOrderFailed { return gerror.New("订单状态已变化,不能重新匹配") } data[oc.OrderStatus], data[oc.MatchedAt] = consts.TradeOrderMatching, nil case consts.TradeActionUnfreeze: if order.FreezeStatus != consts.TradeFreezePending { return gerror.New("订单解冻状态已变化") } data[oc.FreezeStatus], data[oc.OrderStatus], data[oc.UnfrozenAt] = consts.TradeFreezeNormal, consts.TradeOrderPending, gtime.Now() default: return gerror.New("订单处理动作不合法") } if _, err := s.model(ctx).WherePri(order.Id).Data(data).Update(); err != nil { return gerror.Wrap(err, "执行订单处理失败") } ac := dao.ApprovalRequest.Columns() if _, err := service.SysApproval().Model(ctx).WherePri(approval.Id).Data(g.Map{ac.ApprovalStatus: consts.ApprovalStatusExecuted, ac.ExecutedAt: gtime.Now(), ac.UpdatedBy: contexts.GetUserId(ctx), ac.Version: gdb.Raw(ac.Version + "+1")}).Update(); err != nil { return err } return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "trade", Action: "trade_order." + action, TargetType: "trade_order", TargetId: order.Id, Before: order, After: data, Reason: reason, ApprovalNo: approval.ApprovalNo}) }) }