feat: 完善手动派单后台操作
This commit is contained in:
@@ -33,6 +33,21 @@ type MatchReq struct {
|
|||||||
sysin.TradeOrderMatchInp
|
sysin.TradeOrderMatchInp
|
||||||
}
|
}
|
||||||
type MatchRes struct{}
|
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 {
|
type ApplyReq struct {
|
||||||
g.Meta `path:"/tradeOrder/apply" method:"post" tags:"交易订单" summary:"提交订单人工处理审批"`
|
g.Meta `path:"/tradeOrder/apply" method:"post" tags:"交易订单" summary:"提交订单人工处理审批"`
|
||||||
sysin.TradeOrderApplyInp
|
sysin.TradeOrderApplyInp
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ var CustomerServiceOnlineOptions = []*model.Option{
|
|||||||
}
|
}
|
||||||
var TradeAutoDispatchOptions = []*model.Option{
|
var TradeAutoDispatchOptions = []*model.Option{
|
||||||
dict.GenSuccessOption(TradeAutoDispatchEnabled, "自动派单"),
|
dict.GenSuccessOption(TradeAutoDispatchEnabled, "自动派单"),
|
||||||
dict.GenDefaultOption(TradeAutoDispatchDisabled, "手动领取"),
|
dict.GenDefaultOption(TradeAutoDispatchDisabled, "手动派单"),
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -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) {
|
func (c *cTradeOrder) Match(ctx context.Context, req *tradeorder.MatchReq) (*tradeorder.MatchRes, error) {
|
||||||
return &tradeorder.MatchRes{}, service.SysTradeOrder().Match(ctx, &req.TradeOrderMatchInp)
|
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) {
|
func (c *cTradeOrder) Apply(ctx context.Context, req *tradeorder.ApplyReq) (*tradeorder.ApplyRes, error) {
|
||||||
v, err := service.SysTradeOrder().Apply(ctx, &req.TradeOrderApplyInp)
|
v, err := service.SysTradeOrder().Apply(ctx, &req.TradeOrderApplyInp)
|
||||||
return &tradeorder.ApplyRes{FinanceApprovalModel: v}, err
|
return &tradeorder.ApplyRes{FinanceApprovalModel: v}, err
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
"hotgo/internal/consts"
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/dao"
|
"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)
|
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)
|
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))
|
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").
|
return s.model(ctx).As("o").
|
||||||
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("o", oc.MemberId)).
|
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.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.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)).
|
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) {
|
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 {
|
for _, row := range list {
|
||||||
row.CanMatch = row.AutoDispatch == consts.TradeAutoDispatchDisabled && row.OrderStatus == consts.TradeOrderMatching && row.ApprovalRequestId == 0
|
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.CanApply = row.ApprovalRequestId == 0 && row.OrderStatus != consts.TradeOrderCompleted && row.OrderStatus != consts.TradeOrderCancelled
|
||||||
row.CanExecute = row.ApprovalRequestId > 0 && row.ApprovalStatus == consts.ApprovalStatusApproved
|
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("会员等级生效版本不存在")
|
return gerror.New("会员等级生效版本不存在")
|
||||||
}
|
}
|
||||||
totalAmount := product.PriceAmount * int64(in.Quantity)
|
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
|
commissionAmount := totalAmount * int64(levelVersion.CommissionRate) / 10000
|
||||||
afterAvailable := account.AvailableAmount - totalAmount
|
snapshotData := g.Map{}
|
||||||
afterFrozen := account.FrozenAmount + totalAmount
|
if order.RuleSnapshot != nil {
|
||||||
walletResult, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).
|
for key, value := range order.RuleSnapshot.Map() {
|
||||||
WherePri(account.Id).Where(wc.Version, account.Version).
|
snapshotData[key] = value
|
||||||
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 {
|
snapshotData["source"], snapshotData["dispatch_mode"] = "admin_manual_match", "manual"
|
||||||
return gerror.New("会员余额已变化,请刷新后重试")
|
snapshotData["product_id"], snapshotData["product_no"] = product.Id, product.ProductNo
|
||||||
}
|
snapshotData["unit_price_amount"], snapshotData["quantity"] = product.PriceAmount, in.Quantity
|
||||||
if err = insertTradeMatchLedger(ctx, order, account, fmt.Sprintf("WL%dMA", order.Id), "available", consts.WalletDirectionExpense,
|
snapshotData["member_level_id"], snapshotData["level_version_id"] = levelId, levelVersion.Id
|
||||||
totalAmount, account.AvailableAmount, afterAvailable, "match:available", "人工匹配扣除可用余额"); err != nil {
|
snapshotData["commission_rate"], snapshotData["manual_payment_status"] = levelVersion.CommissionRate, "pending"
|
||||||
return err
|
snapshotData["confirmation_required"], snapshotData["deadline_at"] = true, ""
|
||||||
}
|
snapshot := gjson.New(snapshotData)
|
||||||
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,
|
|
||||||
})
|
|
||||||
oc := dao.TradeOrder.Columns()
|
oc := dao.TradeOrder.Columns()
|
||||||
data := g.Map{
|
data := g.Map{
|
||||||
oc.ProductId: product.Id, oc.ProductName: product.Name,
|
oc.ProductId: product.Id, oc.ProductName: product.Name,
|
||||||
oc.Quantity: in.Quantity, oc.UnitPriceAmount: product.PriceAmount, oc.TotalAmount: totalAmount,
|
oc.Quantity: in.Quantity, oc.UnitPriceAmount: product.PriceAmount, oc.TotalAmount: totalAmount,
|
||||||
oc.CommissionAmount: commissionAmount, oc.Currency: product.Currency, oc.MemberLevelId: levelId,
|
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),
|
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()
|
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,
|
func insertTradeMatchLedger(ctx context.Context, order *entity.TradeOrder, account *entity.WalletAccount,
|
||||||
ledgerNo, entryType string, direction int, amount, beforeAmount, afterAmount int64, suffix, remark string) error {
|
ledgerNo, entryType string, direction int, amount, beforeAmount, afterAmount int64, suffix, remark string) error {
|
||||||
lc := dao.WalletLedger.Columns()
|
lc := dao.WalletLedger.Columns()
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ type TradeOrderListModel struct {
|
|||||||
UnfrozenAt *gtime.Time `json:"unfrozenAt"`
|
UnfrozenAt *gtime.Time `json:"unfrozenAt"`
|
||||||
CreatedAt *gtime.Time `json:"createdAt"`
|
CreatedAt *gtime.Time `json:"createdAt"`
|
||||||
ApprovalStatus int `json:"approvalStatus"`
|
ApprovalStatus int `json:"approvalStatus"`
|
||||||
|
PaymentStatus string `json:"paymentStatus"`
|
||||||
CanMatch bool `json:"canMatch"`
|
CanMatch bool `json:"canMatch"`
|
||||||
|
CanChangeAmount bool `json:"canChangeAmount"`
|
||||||
|
CanForcePay bool `json:"canForcePay"`
|
||||||
|
CanFreeze bool `json:"canFreeze"`
|
||||||
CanReview bool `json:"canReview"`
|
CanReview bool `json:"canReview"`
|
||||||
CanApply bool `json:"canApply"`
|
CanApply bool `json:"canApply"`
|
||||||
CanExecute bool `json:"canExecute"`
|
CanExecute bool `json:"canExecute"`
|
||||||
@@ -111,6 +115,20 @@ type TradeOrderMatchInp struct {
|
|||||||
|
|
||||||
func (in *TradeOrderMatchInp) Filter(ctx context.Context) error { return nil }
|
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 {
|
type FinanceApprovalModel struct {
|
||||||
ApprovalId uint64 `json:"approvalId"`
|
ApprovalId uint64 `json:"approvalId"`
|
||||||
ApprovalNo string `json:"approvalNo"`
|
ApprovalNo string `json:"approvalNo"`
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ type (
|
|||||||
View(ctx context.Context, in *sysin.TradeOrderViewInp) (res *sysin.TradeOrderViewModel, err error)
|
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)
|
MatchProducts(ctx context.Context, in *sysin.TradeOrderMatchProductListInp) (list []*sysin.TradeOrderMatchProductModel, totalCount int, err error)
|
||||||
Match(ctx context.Context, in *sysin.TradeOrderMatchInp) 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)
|
Apply(ctx context.Context, in *sysin.TradeOrderApplyInp) (res *sysin.FinanceApprovalModel, err error)
|
||||||
Execute(ctx context.Context, in *sysin.TradeOrderExecuteInp) error
|
Execute(ctx context.Context, in *sysin.TradeOrderExecuteInp) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 TradeOrderView = (params) => http.request({ url: '/tradeOrder/view', method: 'get', params });
|
||||||
export const TradeOrderMatchProducts = (params) => http.request({ url: '/tradeOrder/matchProducts', 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 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 TradeOrderApply = (params) => http.request({ url: '/tradeOrder/apply', method: 'POST', params });
|
||||||
export const TradeOrderExecute = (params) => http.request({ url: '/tradeOrder/execute', method: 'POST', params });
|
export const TradeOrderExecute = (params) => http.request({ url: '/tradeOrder/execute', method: 'POST', params });
|
||||||
|
|
||||||
|
|||||||
@@ -355,7 +355,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, h, onMounted, onUpdated, reactive, ref } from 'vue';
|
import { computed, h, onMounted, onUpdated, reactive, ref } from 'vue';
|
||||||
import { NButton, NSpace, useDialog, useMessage } from 'naive-ui';
|
import { NButton, NSpace, NSwitch, useDialog, useMessage } from 'naive-ui';
|
||||||
import { DeleteOutlined, EditOutlined } from '@vicons/antd';
|
import { DeleteOutlined, EditOutlined } from '@vicons/antd';
|
||||||
import { BasicTable, TableAction } from '@/components/Table';
|
import { BasicTable, TableAction } from '@/components/Table';
|
||||||
import { BasicForm, useForm } from '@/components/Form';
|
import { BasicForm, useForm } from '@/components/Form';
|
||||||
@@ -384,12 +384,33 @@
|
|||||||
NoticeEdit,
|
NoticeEdit,
|
||||||
} from '@/api/appMember';
|
} from '@/api/appMember';
|
||||||
import { List as MemberLevelList } from '@/api/memberLevel';
|
import { List as MemberLevelList } from '@/api/memberLevel';
|
||||||
import { columns, schemas, loadOptions, MemberRecord } from './model';
|
import { columns as baseColumns, schemas, loadOptions, MemberRecord } from './model';
|
||||||
|
|
||||||
const message = useMessage(),
|
const message = useMessage(),
|
||||||
dialog = useDialog(),
|
dialog = useDialog(),
|
||||||
dict = useDictStore();
|
dict = useDictStore();
|
||||||
const { hasPermission } = usePermission();
|
const { hasPermission } = usePermission();
|
||||||
|
const dispatchUpdatingIds = ref(new Set<number>());
|
||||||
|
const columns = baseColumns.map((column) =>
|
||||||
|
column.key === 'autoDispatch'
|
||||||
|
? {
|
||||||
|
...column,
|
||||||
|
render: (row: MemberRecord) =>
|
||||||
|
h(
|
||||||
|
NSwitch,
|
||||||
|
{
|
||||||
|
value: row.autoDispatch,
|
||||||
|
checkedValue: 1,
|
||||||
|
uncheckedValue: 2,
|
||||||
|
loading: dispatchUpdatingIds.value.has(row.id),
|
||||||
|
disabled: !hasPermission(['/appMember/autoDispatch']),
|
||||||
|
'onUpdate:value': (value: number) => toggleDispatch(row, value),
|
||||||
|
},
|
||||||
|
{ checked: () => '自动', unchecked: () => '手动' }
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: column
|
||||||
|
);
|
||||||
const actionRef = ref(),
|
const actionRef = ref(),
|
||||||
searchFormRef = ref<any>({}),
|
searchFormRef = ref<any>({}),
|
||||||
editFormRef = ref<any>(),
|
editFormRef = ref<any>(),
|
||||||
@@ -642,10 +663,17 @@
|
|||||||
detailLoading.value = false;
|
detailLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function toggleDispatch(row) {
|
async function toggleDispatch(row: MemberRecord, nextValue?: number) {
|
||||||
await AutoDispatch({ id: row.id, autoDispatch: row.autoDispatch === 1 ? 2 : 1 });
|
if (dispatchUpdatingIds.value.has(row.id)) return;
|
||||||
message.success('更新成功');
|
const autoDispatch = nextValue ?? (row.autoDispatch === 1 ? 2 : 1);
|
||||||
reloadTable();
|
dispatchUpdatingIds.value.add(row.id);
|
||||||
|
try {
|
||||||
|
await AutoDispatch({ id: row.id, autoDispatch });
|
||||||
|
row.autoDispatch = autoDispatch;
|
||||||
|
message.success(`已切换为${autoDispatch === 1 ? '自动' : '手动'}派单`);
|
||||||
|
} finally {
|
||||||
|
dispatchUpdatingIds.value.delete(row.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function openStatus(row) {
|
function openStatus(row) {
|
||||||
const action = row.memberStatus === 1 ? '禁用' : '启用';
|
const action = row.memberStatus === 1 ? '禁用' : '启用';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="n-layout-page-header"><n-card :bordered="false" title="订单列表">仅“手动领取”且“匹配中”的订单可在操作栏选择商品完成匹配;自动领取订单由系统自动匹配。</n-card></div>
|
<div class="n-layout-page-header"><n-card :bordered="false" title="订单列表">手动派单模式由客服匹配订单,会员确认时校验余额;自动派单模式仅匹配余额内订单并直接完成。</n-card></div>
|
||||||
<n-card :bordered="false" class="proCard">
|
<n-card :bordered="false" class="proCard">
|
||||||
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" />
|
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" />
|
||||||
<BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="(row)=>row.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000">
|
<BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="(row)=>row.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000">
|
||||||
@@ -54,6 +54,15 @@
|
|||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showAmount" preset="card" title="更改金额" class="amount-modal" :mask-closable="false">
|
||||||
|
<n-form label-placement="left" :label-width="90">
|
||||||
|
<n-form-item label="用户名称"><n-input :value="amountMember.username" disabled /></n-form-item>
|
||||||
|
<n-form-item label="账号余额"><n-input :value="yuanPlain(amountMember.availableAmount)" disabled /></n-form-item>
|
||||||
|
<n-form-item label="更改金额"><n-input-number v-model:value="amountForm.amount" :min="0.01" :precision="2" class="w-full" /></n-form-item>
|
||||||
|
<n-form-item label="更改数量"><n-input-number v-model:value="amountForm.quantity" :min="1" :max="999" class="w-full" /></n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<template #action><n-space><n-button @click="showAmount=false">取消</n-button><n-button type="primary" :loading="amountSubmitting" @click="confirmAmount">确定</n-button></n-space></template>
|
||||||
|
</n-modal>
|
||||||
<n-modal v-model:show="showDetail" preset="card" title="订单详情" class="max-w-900px">
|
<n-modal v-model:show="showDetail" preset="card" title="订单详情" class="max-w-900px">
|
||||||
<n-descriptions v-if="detail" bordered label-placement="left" :column="2">
|
<n-descriptions v-if="detail" bordered label-placement="left" :column="2">
|
||||||
<n-descriptions-item label="订单号">{{ detail.orderNo }}</n-descriptions-item><n-descriptions-item label="会员">{{ detail.username }} / {{ detail.mobile }}</n-descriptions-item>
|
<n-descriptions-item label="订单号">{{ detail.orderNo }}</n-descriptions-item><n-descriptions-item label="会员">{{ detail.username }} / {{ detail.mobile }}</n-descriptions-item>
|
||||||
@@ -67,15 +76,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { NButton, NImage, NTag, useMessage } from 'naive-ui';
|
import { NButton, NImage, NTag, useDialog, useMessage } from 'naive-ui';
|
||||||
import { BasicTable, TableAction } from '@/components/Table';import { BasicForm,useForm } from '@/components/Form';import { adaTableScrollX } from '@/utils/hotgo';
|
import { BasicTable, TableAction } from '@/components/Table';import { BasicForm,useForm } from '@/components/Form';import { adaTableScrollX } from '@/utils/hotgo';
|
||||||
import { TradeOrderList,TradeOrderMatch,TradeOrderMatchProducts,TradeOrderView } from '@/api/trade';import { columns,schemas,loadOptions } from './model';
|
import { TradeOrderForcePay,TradeOrderFreeze,TradeOrderList,TradeOrderMatch,TradeOrderMatchProducts,TradeOrderUpdateAmount,TradeOrderView } from '@/api/trade';import { columns,schemas,loadOptions } from './model';
|
||||||
const message=useMessage();const router=useRouter();const actionRef=ref(),searchFormRef=ref<any>({}),showDetail=ref(false),showMatch=ref(false),detail=ref<any>(),current=ref<any>(),autoRefresh=ref(false),refreshCountdown=ref(10);let timer:any;
|
const message=useMessage();const dialog=useDialog();const router=useRouter();const actionRef=ref(),searchFormRef=ref<any>({}),showDetail=ref(false),showMatch=ref(false),showAmount=ref(false),detail=ref<any>(),current=ref<any>(),autoRefresh=ref(false),refreshCountdown=ref(10);let timer:any;
|
||||||
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3 xl:5'},labelWidth:80,schemas});const loadDataTable=(res)=>TradeOrderList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();const yuan=(v)=>`¥${((v||0)/100).toFixed(2)}`;
|
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3 xl:5'},labelWidth:80,schemas});const loadDataTable=(res)=>TradeOrderList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();const yuan=(v)=>`¥${((v||0)/100).toFixed(2)}`;
|
||||||
const yuanPlain=(v)=>((v||0)/100).toFixed(2);
|
const yuanPlain=(v)=>((v||0)/100).toFixed(2);
|
||||||
const matchLoading=ref(false),productLoading=ref(false),matchSubmitting=ref(false),products=ref<any[]>([]),selectedProduct=ref<any>();
|
const matchLoading=ref(false),productLoading=ref(false),matchSubmitting=ref(false),products=ref<any[]>([]),selectedProduct=ref<any>();
|
||||||
const matchMember=reactive({id:0,username:'',availableAmount:0,frozenAmount:0});
|
const matchMember=reactive({id:0,username:'',availableAmount:0,frozenAmount:0});
|
||||||
const matchForm=reactive({id:0,productId:0,quantity:1});
|
const matchForm=reactive({id:0,productId:0,quantity:1});
|
||||||
|
const amountMember=reactive({username:'',availableAmount:0});
|
||||||
|
const amountForm=reactive<{id:number;amount:number|null;quantity:number}>({id:0,amount:null,quantity:1});
|
||||||
|
const amountSubmitting=ref(false);
|
||||||
const productFilter=reactive<{name:string;priceMin:number|null;priceMax:number|null}>({name:'',priceMin:null,priceMax:null});
|
const productFilter=reactive<{name:string;priceMin:number|null;priceMax:number|null}>({name:'',priceMin:null,priceMax:null});
|
||||||
const productPager=reactive({page:1,pageSize:10,total:0});
|
const productPager=reactive({page:1,pageSize:10,total:0});
|
||||||
const productColumns=[
|
const productColumns=[
|
||||||
@@ -100,12 +112,20 @@ async function confirmMatch(){
|
|||||||
matchSubmitting.value=true;try{await TradeOrderMatch(matchForm);message.success('订单匹配成功');showMatch.value=false;reloadTable()}finally{matchSubmitting.value=false}
|
matchSubmitting.value=true;try{await TradeOrderMatch(matchForm);message.success('订单匹配成功');showMatch.value=false;reloadTable()}finally{matchSubmitting.value=false}
|
||||||
}
|
}
|
||||||
async function view(row){detail.value=await TradeOrderView({id:row.id});showDetail.value=true}
|
async function view(row){detail.value=await TradeOrderView({id:row.id});showDetail.value=true}
|
||||||
const actionColumn=reactive({width:210,title:'操作',key:'action',fixed:'right',render:(row)=>{
|
function openAmount(row){Object.assign(amountMember,{username:row.username,availableAmount:row.availableAmount});Object.assign(amountForm,{id:row.id,amount:Number(row.totalAmount||0)/100,quantity:row.quantity||1});showAmount.value=true}
|
||||||
|
async function confirmAmount(){if(!amountForm.amount||amountForm.amount<=0){message.error('请输入正确金额');return}amountSubmitting.value=true;try{await TradeOrderUpdateAmount({id:amountForm.id,amount:amountForm.amount.toFixed(2),quantity:amountForm.quantity});message.success('订单金额已更改');showAmount.value=false;reloadTable()}finally{amountSubmitting.value=false}}
|
||||||
|
function forcePay(row){dialog.warning({title:'强制付款',content:`确认对订单 ${row.orderNo} 执行强制付款吗?允许付款后余额为负数。`,positiveText:'确认付款',negativeText:'取消',onPositiveClick:async()=>{await TradeOrderForcePay({id:row.id});message.success('强制付款成功');reloadTable()}})}
|
||||||
|
function freezeOrder(row){dialog.warning({title:'冻结订单',content:`确认冻结订单 ${row.orderNo} 吗?冻结后会员需要联系客服处理。`,positiveText:'确认冻结',negativeText:'取消',onPositiveClick:async()=>{await TradeOrderFreeze({id:row.id});message.success('订单已冻结');reloadTable()}})}
|
||||||
|
const actionColumn=reactive({width:300,title:'操作',key:'action',fixed:'right',render:(row)=>{
|
||||||
const actions:any[]=[];
|
const actions:any[]=[];
|
||||||
if(row.canMatch)actions.push({label:'匹配订单',type:'primary',onClick:()=>openMatch(row)});
|
if(row.canMatch)actions.push({label:'匹配订单',type:'primary',onClick:()=>openMatch(row)});
|
||||||
if(row.orderType===2&&row.orderStatus===2&&row.canReview)actions.push({label:'审核任务',type:'warning',onClick:()=>router.push({name:'task_submission_list',query:{tradeOrderId:String(row.id)}})});
|
if(row.canChangeAmount)actions.push({label:'更改金额',type:'primary',onClick:()=>openAmount(row)});
|
||||||
else if(row.orderType===2&&row.orderStatus===2)actions.push({label:'等待会员提交',disabled:true,title:'会员提交任务凭证后即可审核'});
|
if(row.canForcePay)actions.push({label:'强制付款',type:'primary',onClick:()=>forcePay(row)});
|
||||||
actions.push({label:'详情',onClick:()=>view(row)});
|
if(row.canFreeze)actions.push({label:'冻结订单',type:'warning',onClick:()=>freezeOrder(row)});
|
||||||
|
const hasManualActions=row.canChangeAmount||row.canForcePay||row.canFreeze;
|
||||||
|
if(!hasManualActions&&row.orderType===2&&row.orderStatus===2&&row.canReview)actions.push({label:'审核任务',type:'warning',onClick:()=>router.push({name:'task_submission_list',query:{tradeOrderId:String(row.id)}})});
|
||||||
|
else if(!hasManualActions&&row.orderType===2&&row.orderStatus===2)actions.push({label:'等待会员付款',disabled:true,title:'等待会员完成付款'});
|
||||||
|
if(!hasManualActions)actions.push({label:'详情',onClick:()=>view(row)});
|
||||||
return h(TableAction as any,{style:'button',actions});
|
return h(TableAction as any,{style:'button',actions});
|
||||||
}});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
}});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
||||||
watch(autoRefresh,(enabled)=>{
|
watch(autoRefresh,(enabled)=>{
|
||||||
@@ -121,6 +141,7 @@ onBeforeUnmount(()=>clearInterval(timer));onMounted(loadOptions);
|
|||||||
</script>
|
</script>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
:deep(.match-order-modal) { width: min(1040px, 94vw); }
|
:deep(.match-order-modal) { width: min(1040px, 94vw); }
|
||||||
|
:deep(.amount-modal) { width: min(620px, 92vw); }
|
||||||
.match-product-area { width: 100%; }
|
.match-product-area { width: 100%; }
|
||||||
.filter-name { width: 180px; }
|
.filter-name { width: 180px; }
|
||||||
.filter-price { width: 130px; }
|
.filter-price { width: 130px; }
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const columns = [
|
|||||||
{ title: '绑卡姓名', key: 'realNameMask', width: 110 },
|
{ title: '绑卡姓名', key: 'realNameMask', width: 110 },
|
||||||
{ title: '用户名', key: 'username', width: 120 },
|
{ title: '用户名', key: 'username', width: 120 },
|
||||||
{ title: '手机号', key: 'mobile', width: 130 },
|
{ title: '手机号', key: 'mobile', width: 130 },
|
||||||
{ title: '自动派单', key: 'autoDispatch', width: 100, render: (r) => tag('tradeAutoDispatch', r.autoDispatch) },
|
{ title: '派单模式', key: 'autoDispatch', width: 100, render: (r) => tag('tradeAutoDispatch', r.autoDispatch) },
|
||||||
{ title: '余额', key: 'availableAmount', width: 120, render: (r) => yuan(r.availableAmount) },
|
{ title: '余额', key: 'availableAmount', width: 120, render: (r) => yuan(r.availableAmount) },
|
||||||
{ title: '当日交易', key: 'todayTradeCount', width: 90 },
|
{ title: '当日交易', key: 'todayTradeCount', width: 90 },
|
||||||
{ title: '商品/任务', key: 'productName', width: 160, ellipsis: { tooltip: true } },
|
{ title: '商品/任务', key: 'productName', width: 160, ellipsis: { tooltip: true } },
|
||||||
@@ -31,7 +31,7 @@ export const columns = [
|
|||||||
{ title: '解冻时间', key: 'unfrozenAt', width: 180 },
|
{ title: '解冻时间', key: 'unfrozenAt', width: 180 },
|
||||||
{ title: '消息', key: 'message', width: 180, ellipsis: { tooltip: true } },
|
{ title: '消息', key: 'message', width: 180, ellipsis: { tooltip: true } },
|
||||||
{ title: '订单状态', key: 'orderStatus', width: 110, render: (r) => r.orderType === 2 && r.orderStatus === 2
|
{ title: '订单状态', key: 'orderStatus', width: 110, render: (r) => r.orderType === 2 && r.orderStatus === 2
|
||||||
? h(NTag, { type: 'warning', bordered: false }, { default: () => '待会员提交' })
|
? h(NTag, { type: 'warning', bordered: false }, { default: () => '待付款' })
|
||||||
: tag('tradeOrderStatus', r.orderStatus) },
|
: tag('tradeOrderStatus', r.orderStatus) },
|
||||||
{ title: '冻结状态', key: 'freezeStatus', width: 110, render: (r) => tag('tradeFreezeStatus', r.freezeStatus) },
|
{ title: '冻结状态', key: 'freezeStatus', width: 110, render: (r) => tag('tradeFreezeStatus', r.freezeStatus) },
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user