From ad6d0bbe01bd4130921a2bb5678e6d387efff053 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Tue, 13 Oct 2026 22:13:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E6=B4=BE=E5=8D=95=E5=90=8E=E5=8F=B0=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/admin/tradeorder/tradeorder.go | 15 + server/internal/consts/trade.go | 2 +- .../controller/admin/sys/trade_order.go | 9 + server/internal/logic/sys/trade_order.go | 259 +++++++++++++++--- server/internal/model/input/sysin/trade.go | 18 ++ server/internal/service/sys.go | 3 + ...0261013_006_trade_order_manual_actions.sql | 16 ++ web/src/api/trade/index.ts | 3 + web/src/views/app-member/list/index.vue | 40 ++- web/src/views/trade/order/index.vue | 37 ++- web/src/views/trade/order/model.ts | 4 +- 11 files changed, 351 insertions(+), 55 deletions(-) create mode 100644 server/storage/data/migrations/20261013_006_trade_order_manual_actions.sql diff --git a/server/api/admin/tradeorder/tradeorder.go b/server/api/admin/tradeorder/tradeorder.go index a8fe652..ca566aa 100644 --- a/server/api/admin/tradeorder/tradeorder.go +++ b/server/api/admin/tradeorder/tradeorder.go @@ -33,6 +33,21 @@ type MatchReq struct { sysin.TradeOrderMatchInp } type MatchRes struct{} +type UpdateAmountReq struct { + g.Meta `path:"/tradeOrder/updateAmount" method:"post" tags:"交易订单" summary:"更改手动订单金额"` + sysin.TradeOrderUpdateAmountInp +} +type UpdateAmountRes struct{} +type ForcePayReq struct { + g.Meta `path:"/tradeOrder/forcePay" method:"post" tags:"交易订单" summary:"强制支付手动订单"` + sysin.TradeOrderManualActionInp +} +type ForcePayRes struct{} +type FreezeReq struct { + g.Meta `path:"/tradeOrder/freeze" method:"post" tags:"交易订单" summary:"冻结手动订单"` + sysin.TradeOrderManualActionInp +} +type FreezeRes struct{} type ApplyReq struct { g.Meta `path:"/tradeOrder/apply" method:"post" tags:"交易订单" summary:"提交订单人工处理审批"` sysin.TradeOrderApplyInp diff --git a/server/internal/consts/trade.go b/server/internal/consts/trade.go index 6cfad34..cc9f999 100644 --- a/server/internal/consts/trade.go +++ b/server/internal/consts/trade.go @@ -106,7 +106,7 @@ var CustomerServiceOnlineOptions = []*model.Option{ } var TradeAutoDispatchOptions = []*model.Option{ dict.GenSuccessOption(TradeAutoDispatchEnabled, "自动派单"), - dict.GenDefaultOption(TradeAutoDispatchDisabled, "手动领取"), + dict.GenDefaultOption(TradeAutoDispatchDisabled, "手动派单"), } func init() { diff --git a/server/internal/controller/admin/sys/trade_order.go b/server/internal/controller/admin/sys/trade_order.go index aa14bc5..7a061f7 100644 --- a/server/internal/controller/admin/sys/trade_order.go +++ b/server/internal/controller/admin/sys/trade_order.go @@ -45,6 +45,15 @@ func (c *cTradeOrder) MatchProducts(ctx context.Context, req *tradeorder.MatchPr func (c *cTradeOrder) Match(ctx context.Context, req *tradeorder.MatchReq) (*tradeorder.MatchRes, error) { return &tradeorder.MatchRes{}, service.SysTradeOrder().Match(ctx, &req.TradeOrderMatchInp) } +func (c *cTradeOrder) UpdateAmount(ctx context.Context, req *tradeorder.UpdateAmountReq) (*tradeorder.UpdateAmountRes, error) { + return &tradeorder.UpdateAmountRes{}, service.SysTradeOrder().UpdateAmount(ctx, &req.TradeOrderUpdateAmountInp) +} +func (c *cTradeOrder) ForcePay(ctx context.Context, req *tradeorder.ForcePayReq) (*tradeorder.ForcePayRes, error) { + return &tradeorder.ForcePayRes{}, service.SysTradeOrder().ForcePay(ctx, &req.TradeOrderManualActionInp) +} +func (c *cTradeOrder) Freeze(ctx context.Context, req *tradeorder.FreezeReq) (*tradeorder.FreezeRes, error) { + return &tradeorder.FreezeRes{}, service.SysTradeOrder().Freeze(ctx, &req.TradeOrderManualActionInp) +} func (c *cTradeOrder) Apply(ctx context.Context, req *tradeorder.ApplyReq) (*tradeorder.ApplyRes, error) { v, err := service.SysTradeOrder().Apply(ctx, &req.TradeOrderApplyInp) return &tradeorder.ApplyRes{FinanceApprovalModel: v}, err diff --git a/server/internal/logic/sys/trade_order.go b/server/internal/logic/sys/trade_order.go index 705fbc0..3a83358 100644 --- a/server/internal/logic/sys/trade_order.go +++ b/server/internal/logic/sys/trade_order.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "time" "hotgo/internal/consts" "hotgo/internal/dao" @@ -34,12 +35,13 @@ func (s *sSysTradeOrder) listModel(ctx context.Context) *gdb.Model { 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) dispatchMode := fmt.Sprintf("CASE COALESCE(JSON_UNQUOTE(JSON_EXTRACT(%s,'$.dispatch_mode')),'') WHEN 'auto' THEN %d WHEN 'manual' THEN %d ELSE %s END auto_dispatch", col("o", oc.RuleSnapshot), consts.TradeAutoDispatchEnabled, consts.TradeAutoDispatchDisabled, col("m", mc.AutoDispatch)) + paymentStatus := fmt.Sprintf("COALESCE(JSON_UNQUOTE(JSON_EXTRACT(%s,'$.manual_payment_status')),'') payment_status", col("o", oc.RuleSnapshot)) 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.Mobile), col("p", pc.RealNameMask), dispatchMode, 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)) + 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), dispatchMode, paymentStatus, 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) { @@ -68,6 +70,10 @@ func (s *sSysTradeOrder) List(ctx context.Context, in *sysin.TradeOrderListInp) } for _, row := range list { row.CanMatch = row.AutoDispatch == consts.TradeAutoDispatchDisabled && row.OrderStatus == consts.TradeOrderMatching && row.ApprovalRequestId == 0 + manualPending := row.AutoDispatch == consts.TradeAutoDispatchDisabled && row.OrderStatus == consts.TradeOrderPending && row.FreezeStatus == consts.TradeFreezeNormal + row.CanChangeAmount = manualPending + row.CanForcePay = manualPending + row.CanFreeze = manualPending row.CanApply = row.ApprovalRequestId == 0 && row.OrderStatus != consts.TradeOrderCompleted && row.OrderStatus != consts.TradeOrderCancelled row.CanExecute = row.ApprovalRequestId > 0 && row.ApprovalStatus == consts.ApprovalStatusApproved } @@ -204,50 +210,26 @@ 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, "扣除订单本金失败") + snapshotData := g.Map{} + if order.RuleSnapshot != nil { + for key, value := range order.RuleSnapshot.Map() { + snapshotData[key] = value + } } - 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, - "memberLevelId": levelId, "levelVersionId": levelVersion.Id, - "commissionRate": levelVersion.CommissionRate, - }) + snapshotData["source"], snapshotData["dispatch_mode"] = "admin_manual_match", "manual" + snapshotData["product_id"], snapshotData["product_no"] = product.Id, product.ProductNo + snapshotData["unit_price_amount"], snapshotData["quantity"] = product.PriceAmount, in.Quantity + snapshotData["member_level_id"], snapshotData["level_version_id"] = levelId, levelVersion.Id + snapshotData["commission_rate"], snapshotData["manual_payment_status"] = levelVersion.CommissionRate, "pending" + snapshotData["confirmation_required"], snapshotData["deadline_at"] = true, "" + snapshot := gjson.New(snapshotData) oc := dao.TradeOrder.Columns() data := g.Map{ 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() @@ -266,6 +248,207 @@ func (s *sSysTradeOrder) Match(ctx context.Context, in *sysin.TradeOrderMatchInp }) } +func (s *sSysTradeOrder) UpdateAmount(ctx context.Context, in *sysin.TradeOrderUpdateAmountInp) error { + amount, err := parseCents(in.Amount) + if err != nil || amount <= 0 || amount < int64(in.Quantity) { + return gerror.New("更改金额不合法") + } + 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 err := validateManualPaymentOrder(order, false); err != nil { + return err + } + commissionRate := order.RuleSnapshot.Get("commission_rate").Int64() + commissionAmount := amount * commissionRate / 10000 + if order.RuleSnapshot.Get("manual_payment_status").String() == "paid" && amount != order.TotalAmount { + if err := s.adjustPaidManualAmount(ctx, order, amount); err != nil { + return err + } + } + snapshot := cloneOrderSnapshot(order) + snapshot["unit_price_amount"] = amount / int64(in.Quantity) + snapshot["quantity"] = in.Quantity + snapshot["manual_order_amount"] = amount + snapshot["amount_updated_at"] = time.Now().UTC().Format(time.RFC3339) + oc := dao.TradeOrder.Columns() + data := g.Map{oc.Quantity: in.Quantity, oc.UnitPriceAmount: amount / int64(in.Quantity), oc.TotalAmount: amount, + oc.CommissionAmount: commissionAmount, oc.RuleSnapshot: gjson.New(snapshot), oc.Message: "后台已更改订单金额,等待会员确认付款", oc.UpdatedBy: contexts.GetUserId(ctx)} + result, err := s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderPending).Data(data).Update() + if err != nil { + return gerror.Wrap(err, "更改订单金额失败") + } + if affected, _ := result.RowsAffected(); affected != 1 { + return gerror.New("订单状态已变化,请刷新后重试") + } + return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "trade", Action: "trade_order.update_amount", + TargetType: "trade_order", TargetId: order.Id, Before: order, After: data, + ChangeJson: gjson.New(g.Map{"amount": amount, "quantity": in.Quantity}), Reason: "后台更改手动订单金额"}) + }) +} + +func (s *sSysTradeOrder) ForcePay(ctx context.Context, in *sysin.TradeOrderManualActionInp) 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 err := validateManualPaymentOrder(order, false); err != nil { + return err + } + if order.RuleSnapshot.Get("manual_payment_status").String() == "paid" { + return nil + } + if order.TotalAmount <= 0 { + return gerror.New("订单金额不合法") + } + wc := dao.WalletAccount.Columns() + var account *entity.WalletAccount + if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}). + Where(wc.MemberId, order.MemberId).Where(wc.Currency, order.Currency).LockUpdate().Scan(&account); err != nil { + return gerror.Wrap(err, "读取会员钱包失败") + } + if account == nil || account.AccountStatus != 1 { + return gerror.New("会员钱包不存在或状态异常") + } + if account.AvailableAmount < math.MinInt64+order.TotalAmount || account.FrozenAmount > math.MaxInt64-order.TotalAmount { + return gerror.New("订单金额超出钱包处理范围") + } + afterAvailable, afterFrozen := account.AvailableAmount-order.TotalAmount, account.FrozenAmount+order.TotalAmount + result, 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, _ := result.RowsAffected(); affected != 1 { + return gerror.New("会员余额已变化,请刷新后重试") + } + if err = insertTradeMatchLedger(ctx, order, account, fmt.Sprintf("WL%dMA", order.Id), "available", consts.WalletDirectionExpense, + order.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, + order.TotalAmount, account.FrozenAmount, afterFrozen, "match:frozen", "手动订单强制付款冻结本金"); err != nil { + return err + } + snapshot := cloneOrderSnapshot(order) + snapshot["manual_payment_status"], snapshot["payment_confirmed_by"] = "paid", "admin" + snapshot["payment_confirmed_at"] = time.Now().UTC().Format(time.RFC3339) + oc := dao.TradeOrder.Columns() + data := g.Map{oc.RuleSnapshot: gjson.New(snapshot), oc.Message: "后台已强制付款,等待会员提交任务", oc.UpdatedBy: contexts.GetUserId(ctx)} + result, err = s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderPending).Data(data).Update() + if err != nil { + return gerror.Wrap(err, "更新订单付款状态失败") + } + if affected, _ := result.RowsAffected(); affected != 1 { + return gerror.New("订单状态已变化,请刷新后重试") + } + return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "trade", Action: "trade_order.force_pay", + TargetType: "trade_order", TargetId: order.Id, Before: order, After: data, Reason: "后台强制支付手动订单"}) + }) +} + +func (s *sSysTradeOrder) adjustPaidManualAmount(ctx context.Context, order *entity.TradeOrder, amount int64) error { + delta := amount - order.TotalAmount + if delta == 0 { + return nil + } + wc := dao.WalletAccount.Columns() + var account *entity.WalletAccount + if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}). + Where(wc.MemberId, order.MemberId).Where(wc.Currency, order.Currency).LockUpdate().Scan(&account); err != nil { + return gerror.Wrap(err, "读取会员钱包失败") + } + if account == nil || account.AccountStatus != 1 { + return gerror.New("会员钱包不存在或状态异常") + } + if delta > 0 && (account.AvailableAmount < math.MinInt64+delta || account.FrozenAmount > math.MaxInt64-delta) { + return gerror.New("更改金额超出钱包处理范围") + } + if delta < 0 { + refund := -delta + if account.FrozenAmount < refund || account.AvailableAmount > math.MaxInt64-refund { + return gerror.New("已冻结本金不足,无法减少订单金额") + } + } + afterAvailable, afterFrozen := account.AvailableAmount-delta, account.FrozenAmount+delta + result, 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, _ := result.RowsAffected(); affected != 1 { + return gerror.New("会员余额已变化,请刷新后重试") + } + tag := fmt.Sprintf("%x", time.Now().UnixNano()) + ledgerAmount := delta + availableDirection, frozenDirection := consts.WalletDirectionExpense, consts.WalletDirectionIncome + if ledgerAmount < 0 { + ledgerAmount = -ledgerAmount + availableDirection, frozenDirection = consts.WalletDirectionIncome, consts.WalletDirectionExpense + } + if err = insertTradeMatchLedger(ctx, order, account, fmt.Sprintf("WL%dUA%s", order.Id, tag), "available", availableDirection, + ledgerAmount, account.AvailableAmount, afterAvailable, "amount:update:"+tag+":available", "手动订单更改金额调整可用余额"); err != nil { + return err + } + if err = insertTradeMatchLedger(ctx, order, account, fmt.Sprintf("WL%dUF%s", order.Id, tag), "frozen", frozenDirection, + ledgerAmount, account.FrozenAmount, afterFrozen, "amount:update:"+tag+":frozen", "手动订单更改金额调整冻结本金"); err != nil { + return err + } + return nil +} + +func (s *sSysTradeOrder) Freeze(ctx context.Context, in *sysin.TradeOrderManualActionInp) 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 err := validateManualPaymentOrder(order, false); err != nil { + return err + } + snapshot := cloneOrderSnapshot(order) + snapshot["frozen_by"], snapshot["frozen_at"] = "admin", time.Now().UTC().Format(time.RFC3339) + oc := dao.TradeOrder.Columns() + data := g.Map{oc.OrderStatus: consts.TradeOrderFrozen, oc.FreezeStatus: consts.TradeFreezeFrozen, + oc.RuleSnapshot: gjson.New(snapshot), oc.Message: "订单已被后台冻结,请联系客服", oc.UpdatedBy: contexts.GetUserId(ctx)} + result, err := s.model(ctx).WherePri(order.Id).Where(oc.OrderStatus, consts.TradeOrderPending).Data(data).Update() + if err != nil { + return gerror.Wrap(err, "冻结订单失败") + } + if affected, _ := result.RowsAffected(); affected != 1 { + return gerror.New("订单状态已变化,请刷新后重试") + } + return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "trade", Action: "trade_order.freeze", + TargetType: "trade_order", TargetId: order.Id, Before: order, After: data, Reason: "后台冻结手动订单"}) + }) +} + +func validateManualPaymentOrder(order *entity.TradeOrder, requireUnpaid bool) error { + if order == nil { + return gerror.New("交易订单不存在") + } + if order.OrderStatus != consts.TradeOrderPending || order.FreezeStatus != consts.TradeFreezeNormal || order.RuleSnapshot == nil || order.RuleSnapshot.Get("dispatch_mode").String() != "manual" { + return gerror.New("当前订单状态不允许此操作") + } + if requireUnpaid && order.RuleSnapshot.Get("manual_payment_status").String() == "paid" { + return gerror.New("订单已经付款") + } + return nil +} + +func cloneOrderSnapshot(order *entity.TradeOrder) g.Map { + snapshot := g.Map{} + if order != nil && order.RuleSnapshot != nil { + for key, value := range order.RuleSnapshot.Map() { + snapshot[key] = value + } + } + return snapshot +} + 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() diff --git a/server/internal/model/input/sysin/trade.go b/server/internal/model/input/sysin/trade.go index ff2bc7e..e5b6ef1 100644 --- a/server/internal/model/input/sysin/trade.go +++ b/server/internal/model/input/sysin/trade.go @@ -52,7 +52,11 @@ type TradeOrderListModel struct { UnfrozenAt *gtime.Time `json:"unfrozenAt"` CreatedAt *gtime.Time `json:"createdAt"` ApprovalStatus int `json:"approvalStatus"` + PaymentStatus string `json:"paymentStatus"` CanMatch bool `json:"canMatch"` + CanChangeAmount bool `json:"canChangeAmount"` + CanForcePay bool `json:"canForcePay"` + CanFreeze bool `json:"canFreeze"` CanReview bool `json:"canReview"` CanApply bool `json:"canApply"` CanExecute bool `json:"canExecute"` @@ -111,6 +115,20 @@ type TradeOrderMatchInp struct { func (in *TradeOrderMatchInp) Filter(ctx context.Context) error { return nil } +type TradeOrderUpdateAmountInp struct { + Id uint64 `json:"id" v:"required|min:1#订单ID不能为空|订单ID不能为空"` + Amount string `json:"amount" v:"required#更改金额不能为空"` + Quantity uint `json:"quantity" v:"required|min:1|max:999#更改数量不能为空|更改数量最少为1|更改数量不能超过999"` +} + +func (in *TradeOrderUpdateAmountInp) Filter(ctx context.Context) error { return nil } + +type TradeOrderManualActionInp struct { + Id uint64 `json:"id" v:"required|min:1#订单ID不能为空|订单ID不能为空"` +} + +func (in *TradeOrderManualActionInp) Filter(ctx context.Context) error { return nil } + type FinanceApprovalModel struct { ApprovalId uint64 `json:"approvalId"` ApprovalNo string `json:"approvalNo"` diff --git a/server/internal/service/sys.go b/server/internal/service/sys.go index 67d569f..8e586bd 100644 --- a/server/internal/service/sys.go +++ b/server/internal/service/sys.go @@ -45,6 +45,9 @@ type ( View(ctx context.Context, in *sysin.TradeOrderViewInp) (res *sysin.TradeOrderViewModel, err error) MatchProducts(ctx context.Context, in *sysin.TradeOrderMatchProductListInp) (list []*sysin.TradeOrderMatchProductModel, totalCount int, err error) Match(ctx context.Context, in *sysin.TradeOrderMatchInp) error + UpdateAmount(ctx context.Context, in *sysin.TradeOrderUpdateAmountInp) error + ForcePay(ctx context.Context, in *sysin.TradeOrderManualActionInp) error + Freeze(ctx context.Context, in *sysin.TradeOrderManualActionInp) error Apply(ctx context.Context, in *sysin.TradeOrderApplyInp) (res *sysin.FinanceApprovalModel, err error) Execute(ctx context.Context, in *sysin.TradeOrderExecuteInp) error } diff --git a/server/storage/data/migrations/20261013_006_trade_order_manual_actions.sql b/server/storage/data/migrations/20261013_006_trade_order_manual_actions.sql new file mode 100644 index 0000000..02d107f --- /dev/null +++ b/server/storage/data/migrations/20261013_006_trade_order_manual_actions.sql @@ -0,0 +1,16 @@ +-- 手动派单订单操作权限,挂载到订单管理菜单并继承已有角色权限。 +SET @now=NOW(); +SET @tradeRoot=(SELECT `id` FROM `hg_admin_menu` WHERE `name`='trade_management' LIMIT 1); +SET @orderMenu=(SELECT `id` FROM `hg_admin_menu` WHERE `name`='trade_order' LIMIT 1); + +INSERT INTO `hg_admin_menu` +(`pid`,`level`,`tree`,`title`,`name`,`path`,`icon`,`type`,`redirect`,`permissions`,`permission_name`,`component`,`always_show`,`active_menu`,`is_root`,`is_frame`,`frame_src`,`keep_alive`,`hidden`,`affix`,`sort`,`remark`,`status`,`created_at`,`updated_at`) +SELECT @orderMenu,3,CONCAT('tr_',@tradeRoot,' tr_',@orderMenu,' '),'手动订单操作','trade_order_manual_actions','','',3,'','/tradeOrder/updateAmount,/tradeOrder/forcePay,/tradeOrder/freeze','','',1,'',0,0,'',0,1,0,6,'更改金额、强制付款、冻结订单权限',1,@now,@now +WHERE @orderMenu IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name`='trade_order_manual_actions'); + +SET @actionMenu=(SELECT `id` FROM `hg_admin_menu` WHERE `name`='trade_order_manual_actions' LIMIT 1); + +INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`,`menu_id`) +SELECT DISTINCT rm.`role_id`,@actionMenu +FROM `hg_admin_role_menu` rm +WHERE rm.`menu_id`=@orderMenu AND @actionMenu IS NOT NULL; diff --git a/web/src/api/trade/index.ts b/web/src/api/trade/index.ts index 947e787..70f8acd 100644 --- a/web/src/api/trade/index.ts +++ b/web/src/api/trade/index.ts @@ -4,6 +4,9 @@ export const TradeOrderList = (params) => http.request({ url: '/tradeOrder/list' export const TradeOrderView = (params) => http.request({ url: '/tradeOrder/view', method: 'get', params }); export const TradeOrderMatchProducts = (params) => http.request({ url: '/tradeOrder/matchProducts', method: 'get', params }); export const TradeOrderMatch = (params) => http.request({ url: '/tradeOrder/match', method: 'POST', params }); +export const TradeOrderUpdateAmount = (params) => http.request({ url: '/tradeOrder/updateAmount', method: 'POST', params }); +export const TradeOrderForcePay = (params) => http.request({ url: '/tradeOrder/forcePay', method: 'POST', params }); +export const TradeOrderFreeze = (params) => http.request({ url: '/tradeOrder/freeze', method: 'POST', params }); export const TradeOrderApply = (params) => http.request({ url: '/tradeOrder/apply', method: 'POST', params }); export const TradeOrderExecute = (params) => http.request({ url: '/tradeOrder/execute', method: 'POST', params }); diff --git a/web/src/views/app-member/list/index.vue b/web/src/views/app-member/list/index.vue index fdbb31c..a3ea5c7 100644 --- a/web/src/views/app-member/list/index.vue +++ b/web/src/views/app-member/list/index.vue @@ -355,7 +355,7 @@