feat: add order governance flags and reconciliation

This commit is contained in:
2026-01-16 13:29:59 +08:00
parent 7ead7fc11c
commit e5f40287c3
17 changed files with 1247 additions and 103 deletions

View File

@@ -165,6 +165,10 @@ type SuperOrderListFilter struct {
Type *consts.OrderType `query:"type"` Type *consts.OrderType `query:"type"`
// Status 订单状态过滤。 // Status 订单状态过滤。
Status *consts.OrderStatus `query:"status"` Status *consts.OrderStatus `query:"status"`
// IsFlagged 是否标记为问题订单true/false
IsFlagged *bool `query:"is_flagged"`
// IsReconciled 是否已完成对账true/false
IsReconciled *bool `query:"is_reconciled"`
// CreatedAtFrom 创建时间起始RFC3339 // CreatedAtFrom 创建时间起始RFC3339
CreatedAtFrom *string `query:"created_at_from"` CreatedAtFrom *string `query:"created_at_from"`
// CreatedAtTo 创建时间结束RFC3339 // CreatedAtTo 创建时间结束RFC3339
@@ -549,6 +553,22 @@ type SuperOrderItem struct {
PaidAt string `json:"paid_at"` PaidAt string `json:"paid_at"`
// RefundedAt 退款时间RFC3339 // RefundedAt 退款时间RFC3339
RefundedAt string `json:"refunded_at"` RefundedAt string `json:"refunded_at"`
// IsFlagged 是否标记为问题订单。
IsFlagged bool `json:"is_flagged"`
// FlagReason 问题标记原因。
FlagReason string `json:"flag_reason"`
// FlaggedBy 标记操作者ID。
FlaggedBy int64 `json:"flagged_by"`
// FlaggedAt 标记时间RFC3339
FlaggedAt string `json:"flagged_at"`
// IsReconciled 是否完成对账。
IsReconciled bool `json:"is_reconciled"`
// ReconcileNote 对账说明。
ReconcileNote string `json:"reconcile_note"`
// ReconciledBy 对账操作者ID。
ReconciledBy int64 `json:"reconciled_by"`
// ReconciledAt 对账时间RFC3339
ReconciledAt string `json:"reconciled_at"`
// CreatedAt 创建时间RFC3339 // CreatedAt 创建时间RFC3339
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
// UpdatedAt 更新时间RFC3339 // UpdatedAt 更新时间RFC3339
@@ -621,6 +641,20 @@ type SuperOrderRefundForm struct {
IdempotencyKey string `json:"idempotency_key"` IdempotencyKey string `json:"idempotency_key"`
} }
type SuperOrderFlagForm struct {
// IsFlagged 是否标记为问题订单。
IsFlagged bool `json:"is_flagged"`
// Reason 标记原因(标记为问题时必填)。
Reason string `json:"reason"`
}
type SuperOrderReconcileForm struct {
// IsReconciled 是否完成对账。
IsReconciled bool `json:"is_reconciled"`
// Note 对账说明(可选)。
Note string `json:"note"`
}
// AdminContentItem for super admin view // AdminContentItem for super admin view
type AdminContentItem struct { type AdminContentItem struct {
// Content 内容摘要信息。 // Content 内容摘要信息。

View File

@@ -4,6 +4,7 @@ import (
dto "quyun/v2/app/http/super/v1/dto" dto "quyun/v2/app/http/super/v1/dto"
"quyun/v2/app/requests" "quyun/v2/app/requests"
"quyun/v2/app/services" "quyun/v2/app/services"
"quyun/v2/database/models"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -42,6 +43,42 @@ func (c *orders) Get(ctx fiber.Ctx, id int64) (*dto.SuperOrderDetail, error) {
return services.Super.GetOrder(ctx, id) return services.Super.GetOrder(ctx, id)
} }
// Flag order
//
// @Router /super/v1/orders/:id<int>/flag [post]
// @Summary Flag order
// @Description Flag or unflag an order as problematic
// @Tags Order
// @Accept json
// @Produce json
// @Param id path int64 true "Order ID"
// @Param form body dto.SuperOrderFlagForm true "Flag form"
// @Success 200 {string} string "OK"
// @Bind user local key(__ctx_user)
// @Bind id path
// @Bind form body
func (c *orders) Flag(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperOrderFlagForm) error {
return services.Super.FlagOrder(ctx, user.ID, id, form)
}
// Reconcile order
//
// @Router /super/v1/orders/:id<int>/reconcile [post]
// @Summary Reconcile order
// @Description Mark or unmark order reconciliation status
// @Tags Order
// @Accept json
// @Produce json
// @Param id path int64 true "Order ID"
// @Param form body dto.SuperOrderReconcileForm true "Reconcile form"
// @Success 200 {string} string "OK"
// @Bind user local key(__ctx_user)
// @Bind id path
// @Bind form body
func (c *orders) Reconcile(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperOrderReconcileForm) error {
return services.Super.ReconcileOrder(ctx, user.ID, id, form)
}
// Refund order // Refund order
// //
// @Router /super/v1/orders/:id<int>/refund [post] // @Router /super/v1/orders/:id<int>/refund [post]

View File

@@ -267,6 +267,20 @@ func (r *Routes) Register(router fiber.Router) {
router.Get("/super/v1/orders/statistics"[len(r.Path()):], DataFunc0( router.Get("/super/v1/orders/statistics"[len(r.Path()):], DataFunc0(
r.orders.Statistics, r.orders.Statistics,
)) ))
r.log.Debugf("Registering route: Post /super/v1/orders/:id<int>/flag -> orders.Flag")
router.Post("/super/v1/orders/:id<int>/flag"[len(r.Path()):], Func3(
r.orders.Flag,
Local[*models.User]("__ctx_user"),
PathParam[int64]("id"),
Body[dto.SuperOrderFlagForm]("form"),
))
r.log.Debugf("Registering route: Post /super/v1/orders/:id<int>/reconcile -> orders.Reconcile")
router.Post("/super/v1/orders/:id<int>/reconcile"[len(r.Path()):], Func3(
r.orders.Reconcile,
Local[*models.User]("__ctx_user"),
PathParam[int64]("id"),
Body[dto.SuperOrderReconcileForm]("form"),
))
r.log.Debugf("Registering route: Post /super/v1/orders/:id<int>/refund -> orders.Refund") r.log.Debugf("Registering route: Post /super/v1/orders/:id<int>/refund -> orders.Refund")
router.Post("/super/v1/orders/:id<int>/refund"[len(r.Path()):], Func2( router.Post("/super/v1/orders/:id<int>/refund"[len(r.Path()):], Func2(
r.orders.Refund, r.orders.Refund,

View File

@@ -4529,6 +4529,12 @@ func (s *super) ListOrders(ctx context.Context, filter *super_dto.SuperOrderList
if filter.Status != nil && *filter.Status != "" { if filter.Status != nil && *filter.Status != "" {
q = q.Where(tbl.Status.Eq(*filter.Status)) q = q.Where(tbl.Status.Eq(*filter.Status))
} }
if filter.IsFlagged != nil {
q = q.Where(tbl.IsFlagged.Is(*filter.IsFlagged))
}
if filter.IsReconciled != nil {
q = q.Where(tbl.IsReconciled.Is(*filter.IsReconciled))
}
if filter.AmountPaidMin != nil { if filter.AmountPaidMin != nil {
q = q.Where(tbl.AmountPaid.Gte(*filter.AmountPaidMin)) q = q.Where(tbl.AmountPaid.Gte(*filter.AmountPaidMin))
} }
@@ -4715,6 +4721,140 @@ func (s *super) GetOrder(ctx context.Context, id int64) (*super_dto.SuperOrderDe
}, nil }, nil
} }
func (s *super) FlagOrder(ctx context.Context, operatorID, id int64, form *super_dto.SuperOrderFlagForm) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
}
if id == 0 {
return errorx.ErrBadRequest.WithMsg("订单ID不能为空")
}
if form == nil {
return errorx.ErrBadRequest.WithMsg("标记参数不能为空")
}
reason := strings.TrimSpace(form.Reason)
if form.IsFlagged && reason == "" {
return errorx.ErrBadRequest.WithMsg("标记原因不能为空")
}
tenantID := int64(0)
err := models.Q.Transaction(func(tx *models.Query) error {
tbl, q := tx.Order.QueryContext(ctx)
o, err := q.Where(tbl.ID.Eq(id)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound
}
return errorx.ErrDatabaseError.WithCause(err)
}
tenantID = o.TenantID
// 运营治理:问题订单标记/取消标记需记录操作者与时间,并清空无效原因。
now := time.Now()
updates := map[string]interface{}{
"is_flagged": form.IsFlagged,
"flag_reason": reason,
"flagged_by": operatorID,
"updated_at": now,
}
if form.IsFlagged {
updates["flagged_at"] = now
} else {
updates["flag_reason"] = ""
updates["flagged_by"] = int64(0)
updates["flagged_at"] = nil
}
if _, err := q.Where(tbl.ID.Eq(id)).Updates(updates); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
})
if err != nil {
return err
}
if Audit != nil {
action := "flag_order"
detail := "标记问题订单"
if !form.IsFlagged {
action = "unflag_order"
detail = "取消问题订单标记"
}
if reason != "" {
detail += "" + reason
}
Audit.Log(ctx, tenantID, operatorID, action, cast.ToString(id), detail)
}
return nil
}
func (s *super) ReconcileOrder(ctx context.Context, operatorID, id int64, form *super_dto.SuperOrderReconcileForm) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
}
if id == 0 {
return errorx.ErrBadRequest.WithMsg("订单ID不能为空")
}
if form == nil {
return errorx.ErrBadRequest.WithMsg("对账参数不能为空")
}
note := strings.TrimSpace(form.Note)
tenantID := int64(0)
err := models.Q.Transaction(func(tx *models.Query) error {
tbl, q := tx.Order.QueryContext(ctx)
o, err := q.Where(tbl.ID.Eq(id)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound
}
return errorx.ErrDatabaseError.WithCause(err)
}
tenantID = o.TenantID
// 运营对账:对账与撤销对账需同步更新时间与操作者信息。
now := time.Now()
updates := map[string]interface{}{
"is_reconciled": form.IsReconciled,
"reconcile_note": note,
"reconciled_by": operatorID,
"updated_at": now,
}
if form.IsReconciled {
updates["reconciled_at"] = now
} else {
updates["reconcile_note"] = ""
updates["reconciled_by"] = int64(0)
updates["reconciled_at"] = nil
}
if _, err := q.Where(tbl.ID.Eq(id)).Updates(updates); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
})
if err != nil {
return err
}
if Audit != nil {
action := "reconcile_order"
detail := "完成订单对账"
if !form.IsReconciled {
action = "unreconcile_order"
detail = "撤销订单对账"
}
if note != "" {
detail += "" + note
}
Audit.Log(ctx, tenantID, operatorID, action, cast.ToString(id), detail)
}
return nil
}
func (s *super) RefundOrder(ctx context.Context, id int64, form *super_dto.SuperOrderRefundForm) error { func (s *super) RefundOrder(ctx context.Context, id int64, form *super_dto.SuperOrderRefundForm) error {
o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).First() o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).First()
if err != nil { if err != nil {
@@ -4939,6 +5079,12 @@ func (s *super) toSuperOrderItem(o *models.Order, tenant *models.Tenant, buyer *
AmountDiscount: o.AmountDiscount, AmountDiscount: o.AmountDiscount,
AmountPaid: o.AmountPaid, AmountPaid: o.AmountPaid,
Snapshot: o.Snapshot.Data(), Snapshot: o.Snapshot.Data(),
IsFlagged: o.IsFlagged,
FlagReason: o.FlagReason,
FlaggedBy: o.FlaggedBy,
IsReconciled: o.IsReconciled,
ReconcileNote: o.ReconcileNote,
ReconciledBy: o.ReconciledBy,
CreatedAt: o.CreatedAt.Format(time.RFC3339), CreatedAt: o.CreatedAt.Format(time.RFC3339),
UpdatedAt: o.UpdatedAt.Format(time.RFC3339), UpdatedAt: o.UpdatedAt.Format(time.RFC3339),
} }
@@ -4949,6 +5095,12 @@ func (s *super) toSuperOrderItem(o *models.Order, tenant *models.Tenant, buyer *
if !o.RefundedAt.IsZero() { if !o.RefundedAt.IsZero() {
item.RefundedAt = o.RefundedAt.Format(time.RFC3339) item.RefundedAt = o.RefundedAt.Format(time.RFC3339)
} }
if !o.FlaggedAt.IsZero() {
item.FlaggedAt = o.FlaggedAt.Format(time.RFC3339)
}
if !o.ReconciledAt.IsZero() {
item.ReconciledAt = o.ReconciledAt.Format(time.RFC3339)
}
if tenant != nil { if tenant != nil {
item.Tenant = &super_dto.OrderTenantLite{ item.Tenant = &super_dto.OrderTenantLite{

View File

@@ -2,10 +2,12 @@ package services
import ( import (
"database/sql" "database/sql"
"errors"
"testing" "testing"
"time" "time"
"quyun/v2/app/commands/testx" "quyun/v2/app/commands/testx"
"quyun/v2/app/errorx"
super_dto "quyun/v2/app/http/super/v1/dto" super_dto "quyun/v2/app/http/super/v1/dto"
"quyun/v2/app/requests" "quyun/v2/app/requests"
"quyun/v2/database" "quyun/v2/database"
@@ -597,3 +599,92 @@ func (s *SuperTestSuite) Test_ContentReview() {
So(reloaded2.Status, ShouldEqual, consts.ContentStatusBlocked) So(reloaded2.Status, ShouldEqual, consts.ContentStatusBlocked)
}) })
} }
func (s *SuperTestSuite) Test_OrderGovernance() {
Convey("OrderGovernance", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameOrder, models.TableNameAuditLog)
newOrder := func() *models.Order {
o := &models.Order{
TenantID: 1,
UserID: 2,
Type: consts.OrderTypeContentPurchase,
Status: consts.OrderStatusPaid,
AmountPaid: 100,
}
So(models.OrderQuery.WithContext(ctx).Create(o), ShouldBeNil)
return o
}
operatorID := int64(9001)
Convey("should require reason when flagging", func() {
o := newOrder()
err := Super.FlagOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderFlagForm{
IsFlagged: true,
})
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrBadRequest.Code)
})
Convey("should flag and unflag order", func() {
o := newOrder()
reason := "支付回调异常"
So(Super.FlagOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderFlagForm{
IsFlagged: true,
Reason: reason,
}), ShouldBeNil)
reloaded, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First()
So(err, ShouldBeNil)
So(reloaded.IsFlagged, ShouldBeTrue)
So(reloaded.FlagReason, ShouldEqual, reason)
So(reloaded.FlaggedBy, ShouldEqual, operatorID)
So(reloaded.FlaggedAt.IsZero(), ShouldBeFalse)
So(Super.FlagOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderFlagForm{
IsFlagged: false,
}), ShouldBeNil)
reloaded, err = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First()
So(err, ShouldBeNil)
So(reloaded.IsFlagged, ShouldBeFalse)
So(reloaded.FlagReason, ShouldEqual, "")
So(reloaded.FlaggedBy, ShouldEqual, int64(0))
So(reloaded.FlaggedAt.IsZero(), ShouldBeTrue)
})
Convey("should reconcile and unreconcile order", func() {
o := newOrder()
note := "对账完成"
So(Super.ReconcileOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderReconcileForm{
IsReconciled: true,
Note: note,
}), ShouldBeNil)
reloaded, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First()
So(err, ShouldBeNil)
So(reloaded.IsReconciled, ShouldBeTrue)
So(reloaded.ReconcileNote, ShouldEqual, note)
So(reloaded.ReconciledBy, ShouldEqual, operatorID)
So(reloaded.ReconciledAt.IsZero(), ShouldBeFalse)
So(Super.ReconcileOrder(ctx, operatorID, o.ID, &super_dto.SuperOrderReconcileForm{
IsReconciled: false,
}), ShouldBeNil)
reloaded, err = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(o.ID)).First()
So(err, ShouldBeNil)
So(reloaded.IsReconciled, ShouldBeFalse)
So(reloaded.ReconcileNote, ShouldEqual, "")
So(reloaded.ReconciledBy, ShouldEqual, int64(0))
So(reloaded.ReconciledAt.IsZero(), ShouldBeTrue)
})
})
}

View File

@@ -0,0 +1,39 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE orders
ADD COLUMN IF NOT EXISTS is_flagged BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS flag_reason VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS flagged_by BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS flagged_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS is_reconciled BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS reconcile_note VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS reconciled_by BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS reconciled_at TIMESTAMPTZ;
COMMENT ON COLUMN orders.is_flagged IS '问题订单标记;用途:运营标注需复核订单;默认 false。';
COMMENT ON COLUMN orders.flag_reason IS '问题标记原因;用途:说明问题点与风险;默认空字符串。';
COMMENT ON COLUMN orders.flagged_by IS '问题标记操作者ID用途审计追踪默认 0 表示未标记。';
COMMENT ON COLUMN orders.flagged_at IS '问题标记时间;用途:记录标记时效;未标记为空。';
COMMENT ON COLUMN orders.is_reconciled IS '对账状态;用途:标识是否完成人工对账;默认 false。';
COMMENT ON COLUMN orders.reconcile_note IS '对账说明;用途:记录对账备注与结论;默认空字符串。';
COMMENT ON COLUMN orders.reconciled_by IS '对账操作者ID用途审计追踪默认 0 表示未对账。';
COMMENT ON COLUMN orders.reconciled_at IS '对账时间;用途:记录完成对账时间;未对账为空。';
CREATE INDEX IF NOT EXISTS orders_is_flagged_idx ON orders(is_flagged);
CREATE INDEX IF NOT EXISTS orders_is_reconciled_idx ON orders(is_reconciled);
CREATE INDEX IF NOT EXISTS orders_flagged_at_idx ON orders(flagged_at);
CREATE INDEX IF NOT EXISTS orders_reconciled_at_idx ON orders(reconciled_at);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE orders
DROP COLUMN IF EXISTS reconciled_at,
DROP COLUMN IF EXISTS reconciled_by,
DROP COLUMN IF EXISTS reconcile_note,
DROP COLUMN IF EXISTS is_reconciled,
DROP COLUMN IF EXISTS flagged_at,
DROP COLUMN IF EXISTS flagged_by,
DROP COLUMN IF EXISTS flag_reason,
DROP COLUMN IF EXISTS is_flagged;
-- +goose StatementEnd

View File

@@ -40,9 +40,9 @@ type Content struct {
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"`
Key string `gorm:"column:key;type:character varying(32);comment:Musical key/tone" json:"key"` // Musical key/tone Key string `gorm:"column:key;type:character varying(32);comment:Musical key/tone" json:"key"` // Musical key/tone
IsPinned bool `gorm:"column:is_pinned;type:boolean;comment:Whether content is pinned/featured" json:"is_pinned"` // Whether content is pinned/featured IsPinned bool `gorm:"column:is_pinned;type:boolean;comment:Whether content is pinned/featured" json:"is_pinned"` // Whether content is pinned/featured
Comments []*Comment `gorm:"foreignKey:ContentID;references:ID" json:"comments,omitempty"`
Author *User `gorm:"foreignKey:UserID;references:ID" json:"author,omitempty"` Author *User `gorm:"foreignKey:UserID;references:ID" json:"author,omitempty"`
ContentAssets []*ContentAsset `gorm:"foreignKey:ContentID;references:ID" json:"content_assets,omitempty"` ContentAssets []*ContentAsset `gorm:"foreignKey:ContentID;references:ID" json:"content_assets,omitempty"`
Comments []*Comment `gorm:"foreignKey:ContentID;references:ID" json:"comments,omitempty"`
} }
// Quick operations without importing query package // Quick operations without importing query package

View File

@@ -46,6 +46,12 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery {
_contentQuery.DeletedAt = field.NewField(tableName, "deleted_at") _contentQuery.DeletedAt = field.NewField(tableName, "deleted_at")
_contentQuery.Key = field.NewString(tableName, "key") _contentQuery.Key = field.NewString(tableName, "key")
_contentQuery.IsPinned = field.NewBool(tableName, "is_pinned") _contentQuery.IsPinned = field.NewBool(tableName, "is_pinned")
_contentQuery.Comments = contentQueryHasManyComments{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Comments", "Comment"),
}
_contentQuery.Author = contentQueryBelongsToAuthor{ _contentQuery.Author = contentQueryBelongsToAuthor{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@@ -58,12 +64,6 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery {
RelationField: field.NewRelation("ContentAssets", "ContentAsset"), RelationField: field.NewRelation("ContentAssets", "ContentAsset"),
} }
_contentQuery.Comments = contentQueryHasManyComments{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Comments", "Comment"),
}
_contentQuery.fillFieldMap() _contentQuery.fillFieldMap()
return _contentQuery return _contentQuery
@@ -94,12 +94,12 @@ type contentQuery struct {
DeletedAt field.Field DeletedAt field.Field
Key field.String // Musical key/tone Key field.String // Musical key/tone
IsPinned field.Bool // Whether content is pinned/featured IsPinned field.Bool // Whether content is pinned/featured
Author contentQueryBelongsToAuthor Comments contentQueryHasManyComments
Author contentQueryBelongsToAuthor
ContentAssets contentQueryHasManyContentAssets ContentAssets contentQueryHasManyContentAssets
Comments contentQueryHasManyComments
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -195,23 +195,104 @@ func (c *contentQuery) fillFieldMap() {
func (c contentQuery) clone(db *gorm.DB) contentQuery { func (c contentQuery) clone(db *gorm.DB) contentQuery {
c.contentQueryDo.ReplaceConnPool(db.Statement.ConnPool) c.contentQueryDo.ReplaceConnPool(db.Statement.ConnPool)
c.Comments.db = db.Session(&gorm.Session{Initialized: true})
c.Comments.db.Statement.ConnPool = db.Statement.ConnPool
c.Author.db = db.Session(&gorm.Session{Initialized: true}) c.Author.db = db.Session(&gorm.Session{Initialized: true})
c.Author.db.Statement.ConnPool = db.Statement.ConnPool c.Author.db.Statement.ConnPool = db.Statement.ConnPool
c.ContentAssets.db = db.Session(&gorm.Session{Initialized: true}) c.ContentAssets.db = db.Session(&gorm.Session{Initialized: true})
c.ContentAssets.db.Statement.ConnPool = db.Statement.ConnPool c.ContentAssets.db.Statement.ConnPool = db.Statement.ConnPool
c.Comments.db = db.Session(&gorm.Session{Initialized: true})
c.Comments.db.Statement.ConnPool = db.Statement.ConnPool
return c return c
} }
func (c contentQuery) replaceDB(db *gorm.DB) contentQuery { func (c contentQuery) replaceDB(db *gorm.DB) contentQuery {
c.contentQueryDo.ReplaceDB(db) c.contentQueryDo.ReplaceDB(db)
c.Comments.db = db.Session(&gorm.Session{})
c.Author.db = db.Session(&gorm.Session{}) c.Author.db = db.Session(&gorm.Session{})
c.ContentAssets.db = db.Session(&gorm.Session{}) c.ContentAssets.db = db.Session(&gorm.Session{})
c.Comments.db = db.Session(&gorm.Session{})
return c return c
} }
type contentQueryHasManyComments struct {
db *gorm.DB
field.RelationField
}
func (a contentQueryHasManyComments) Where(conds ...field.Expr) *contentQueryHasManyComments {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a contentQueryHasManyComments) WithContext(ctx context.Context) *contentQueryHasManyComments {
a.db = a.db.WithContext(ctx)
return &a
}
func (a contentQueryHasManyComments) Session(session *gorm.Session) *contentQueryHasManyComments {
a.db = a.db.Session(session)
return &a
}
func (a contentQueryHasManyComments) Model(m *Content) *contentQueryHasManyCommentsTx {
return &contentQueryHasManyCommentsTx{a.db.Model(m).Association(a.Name())}
}
func (a contentQueryHasManyComments) Unscoped() *contentQueryHasManyComments {
a.db = a.db.Unscoped()
return &a
}
type contentQueryHasManyCommentsTx struct{ tx *gorm.Association }
func (a contentQueryHasManyCommentsTx) Find() (result []*Comment, err error) {
return result, a.tx.Find(&result)
}
func (a contentQueryHasManyCommentsTx) Append(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Replace(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Delete(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Clear() error {
return a.tx.Clear()
}
func (a contentQueryHasManyCommentsTx) Count() int64 {
return a.tx.Count()
}
func (a contentQueryHasManyCommentsTx) Unscoped() *contentQueryHasManyCommentsTx {
a.tx = a.tx.Unscoped()
return &a
}
type contentQueryBelongsToAuthor struct { type contentQueryBelongsToAuthor struct {
db *gorm.DB db *gorm.DB
@@ -374,87 +455,6 @@ func (a contentQueryHasManyContentAssetsTx) Unscoped() *contentQueryHasManyConte
return &a return &a
} }
type contentQueryHasManyComments struct {
db *gorm.DB
field.RelationField
}
func (a contentQueryHasManyComments) Where(conds ...field.Expr) *contentQueryHasManyComments {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a contentQueryHasManyComments) WithContext(ctx context.Context) *contentQueryHasManyComments {
a.db = a.db.WithContext(ctx)
return &a
}
func (a contentQueryHasManyComments) Session(session *gorm.Session) *contentQueryHasManyComments {
a.db = a.db.Session(session)
return &a
}
func (a contentQueryHasManyComments) Model(m *Content) *contentQueryHasManyCommentsTx {
return &contentQueryHasManyCommentsTx{a.db.Model(m).Association(a.Name())}
}
func (a contentQueryHasManyComments) Unscoped() *contentQueryHasManyComments {
a.db = a.db.Unscoped()
return &a
}
type contentQueryHasManyCommentsTx struct{ tx *gorm.Association }
func (a contentQueryHasManyCommentsTx) Find() (result []*Comment, err error) {
return result, a.tx.Find(&result)
}
func (a contentQueryHasManyCommentsTx) Append(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Replace(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Delete(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Clear() error {
return a.tx.Clear()
}
func (a contentQueryHasManyCommentsTx) Count() int64 {
return a.tx.Count()
}
func (a contentQueryHasManyCommentsTx) Unscoped() *contentQueryHasManyCommentsTx {
a.tx = a.tx.Unscoped()
return &a
}
type contentQueryDo struct{ gen.DO } type contentQueryDo struct{ gen.DO }
func (c contentQueryDo) Debug() *contentQueryDo { func (c contentQueryDo) Debug() *contentQueryDo {

View File

@@ -37,7 +37,15 @@ type Order struct {
RefundReason string `gorm:"column:refund_reason;type:character varying(255)" json:"refund_reason"` RefundReason string `gorm:"column:refund_reason;type:character varying(255)" json:"refund_reason"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"`
CouponID int64 `gorm:"column:coupon_id;type:bigint;comment:使用的优惠券ID (0表示未使用)" json:"coupon_id"` // 使用的优惠券ID (0表示未使用) CouponID int64 `gorm:"column:coupon_id;type:bigint;comment:使用的优惠券ID (0表示未使用)" json:"coupon_id"` // 使用的优惠券ID (0表示未使用)
IsFlagged bool `gorm:"column:is_flagged;type:boolean;not null;comment:问题订单标记;用途:运营标注需复核订单;默认 false。" json:"is_flagged"` // 问题订单标记;用途:运营标注需复核订单;默认 false。
FlagReason string `gorm:"column:flag_reason;type:character varying(255);not null;comment:问题标记原因;用途:说明问题点与风险;默认空字符串。" json:"flag_reason"` // 问题标记原因;用途:说明问题点与风险;默认空字符串。
FlaggedBy int64 `gorm:"column:flagged_by;type:bigint;not null;comment:问题标记操作者ID用途审计追踪默认 0 表示未标记。" json:"flagged_by"` // 问题标记操作者ID用途审计追踪默认 0 表示未标记。
FlaggedAt time.Time `gorm:"column:flagged_at;type:timestamp with time zone;comment:问题标记时间;用途:记录标记时效;未标记为空。" json:"flagged_at"` // 问题标记时间;用途:记录标记时效;未标记为空。
IsReconciled bool `gorm:"column:is_reconciled;type:boolean;not null;comment:对账状态;用途:标识是否完成人工对账;默认 false。" json:"is_reconciled"` // 对账状态;用途:标识是否完成人工对账;默认 false。
ReconcileNote string `gorm:"column:reconcile_note;type:character varying(255);not null;comment:对账说明;用途:记录对账备注与结论;默认空字符串。" json:"reconcile_note"` // 对账说明;用途:记录对账备注与结论;默认空字符串。
ReconciledBy int64 `gorm:"column:reconciled_by;type:bigint;not null;comment:对账操作者ID用途审计追踪默认 0 表示未对账。" json:"reconciled_by"` // 对账操作者ID用途审计追踪默认 0 表示未对账。
ReconciledAt time.Time `gorm:"column:reconciled_at;type:timestamp with time zone;comment:对账时间;用途:记录完成对账时间;未对账为空。" json:"reconciled_at"` // 对账时间;用途:记录完成对账时间;未对账为空。
} }
// Quick operations without importing query package // Quick operations without importing query package

View File

@@ -44,6 +44,14 @@ func newOrder(db *gorm.DB, opts ...gen.DOOption) orderQuery {
_orderQuery.CreatedAt = field.NewTime(tableName, "created_at") _orderQuery.CreatedAt = field.NewTime(tableName, "created_at")
_orderQuery.UpdatedAt = field.NewTime(tableName, "updated_at") _orderQuery.UpdatedAt = field.NewTime(tableName, "updated_at")
_orderQuery.CouponID = field.NewInt64(tableName, "coupon_id") _orderQuery.CouponID = field.NewInt64(tableName, "coupon_id")
_orderQuery.IsFlagged = field.NewBool(tableName, "is_flagged")
_orderQuery.FlagReason = field.NewString(tableName, "flag_reason")
_orderQuery.FlaggedBy = field.NewInt64(tableName, "flagged_by")
_orderQuery.FlaggedAt = field.NewTime(tableName, "flagged_at")
_orderQuery.IsReconciled = field.NewBool(tableName, "is_reconciled")
_orderQuery.ReconcileNote = field.NewString(tableName, "reconcile_note")
_orderQuery.ReconciledBy = field.NewInt64(tableName, "reconciled_by")
_orderQuery.ReconciledAt = field.NewTime(tableName, "reconciled_at")
_orderQuery.fillFieldMap() _orderQuery.fillFieldMap()
@@ -72,7 +80,15 @@ type orderQuery struct {
RefundReason field.String RefundReason field.String
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
CouponID field.Int64 // 使用的优惠券ID (0表示未使用) CouponID field.Int64 // 使用的优惠券ID (0表示未使用)
IsFlagged field.Bool // 问题订单标记;用途:运营标注需复核订单;默认 false。
FlagReason field.String // 问题标记原因;用途:说明问题点与风险;默认空字符串。
FlaggedBy field.Int64 // 问题标记操作者ID用途审计追踪默认 0 表示未标记。
FlaggedAt field.Time // 问题标记时间;用途:记录标记时效;未标记为空。
IsReconciled field.Bool // 对账状态;用途:标识是否完成人工对账;默认 false。
ReconcileNote field.String // 对账说明;用途:记录对账备注与结论;默认空字符串。
ReconciledBy field.Int64 // 对账操作者ID用途审计追踪默认 0 表示未对账。
ReconciledAt field.Time // 对账时间;用途:记录完成对账时间;未对账为空。
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -108,6 +124,14 @@ func (o *orderQuery) updateTableName(table string) *orderQuery {
o.CreatedAt = field.NewTime(table, "created_at") o.CreatedAt = field.NewTime(table, "created_at")
o.UpdatedAt = field.NewTime(table, "updated_at") o.UpdatedAt = field.NewTime(table, "updated_at")
o.CouponID = field.NewInt64(table, "coupon_id") o.CouponID = field.NewInt64(table, "coupon_id")
o.IsFlagged = field.NewBool(table, "is_flagged")
o.FlagReason = field.NewString(table, "flag_reason")
o.FlaggedBy = field.NewInt64(table, "flagged_by")
o.FlaggedAt = field.NewTime(table, "flagged_at")
o.IsReconciled = field.NewBool(table, "is_reconciled")
o.ReconcileNote = field.NewString(table, "reconcile_note")
o.ReconciledBy = field.NewInt64(table, "reconciled_by")
o.ReconciledAt = field.NewTime(table, "reconciled_at")
o.fillFieldMap() o.fillFieldMap()
@@ -138,7 +162,7 @@ func (o *orderQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (o *orderQuery) fillFieldMap() { func (o *orderQuery) fillFieldMap() {
o.fieldMap = make(map[string]field.Expr, 19) o.fieldMap = make(map[string]field.Expr, 27)
o.fieldMap["id"] = o.ID o.fieldMap["id"] = o.ID
o.fieldMap["tenant_id"] = o.TenantID o.fieldMap["tenant_id"] = o.TenantID
o.fieldMap["user_id"] = o.UserID o.fieldMap["user_id"] = o.UserID
@@ -158,6 +182,14 @@ func (o *orderQuery) fillFieldMap() {
o.fieldMap["created_at"] = o.CreatedAt o.fieldMap["created_at"] = o.CreatedAt
o.fieldMap["updated_at"] = o.UpdatedAt o.fieldMap["updated_at"] = o.UpdatedAt
o.fieldMap["coupon_id"] = o.CouponID o.fieldMap["coupon_id"] = o.CouponID
o.fieldMap["is_flagged"] = o.IsFlagged
o.fieldMap["flag_reason"] = o.FlagReason
o.fieldMap["flagged_by"] = o.FlaggedBy
o.fieldMap["flagged_at"] = o.FlaggedAt
o.fieldMap["is_reconciled"] = o.IsReconciled
o.fieldMap["reconcile_note"] = o.ReconcileNote
o.fieldMap["reconciled_by"] = o.ReconciledBy
o.fieldMap["reconciled_at"] = o.ReconciledAt
} }
func (o orderQuery) clone(db *gorm.DB) orderQuery { func (o orderQuery) clone(db *gorm.DB) orderQuery {

View File

@@ -1349,6 +1349,90 @@ const docTemplate = `{
} }
} }
}, },
"/super/v1/orders/{id}/flag": {
"post": {
"description": "Flag or unflag an order as problematic",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Order"
],
"summary": "Flag order",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Order ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Flag form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperOrderFlagForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/orders/{id}/reconcile": {
"post": {
"description": "Mark or unmark order reconciliation status",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Order"
],
"summary": "Reconcile order",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Order ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Reconcile form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperOrderReconcileForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/orders/{id}/refund": { "/super/v1/orders/{id}/refund": {
"post": { "post": {
"description": "Refund order", "description": "Refund order",
@@ -8734,6 +8818,19 @@ const docTemplate = `{
} }
} }
}, },
"dto.SuperOrderFlagForm": {
"type": "object",
"properties": {
"is_flagged": {
"description": "IsFlagged 是否标记为问题订单。",
"type": "boolean"
},
"reason": {
"description": "Reason 标记原因(标记为问题时必填)。",
"type": "string"
}
}
},
"dto.SuperOrderItem": { "dto.SuperOrderItem": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -8769,10 +8866,30 @@ const docTemplate = `{
} }
] ]
}, },
"flag_reason": {
"description": "FlagReason 问题标记原因。",
"type": "string"
},
"flagged_at": {
"description": "FlaggedAt 标记时间RFC3339。",
"type": "string"
},
"flagged_by": {
"description": "FlaggedBy 标记操作者ID。",
"type": "integer"
},
"id": { "id": {
"description": "ID 订单ID。", "description": "ID 订单ID。",
"type": "integer" "type": "integer"
}, },
"is_flagged": {
"description": "IsFlagged 是否标记为问题订单。",
"type": "boolean"
},
"is_reconciled": {
"description": "IsReconciled 是否完成对账。",
"type": "boolean"
},
"items": { "items": {
"description": "Items 订单明细行,用于展示具体内容与金额拆分。", "description": "Items 订单明细行,用于展示具体内容与金额拆分。",
"type": "array", "type": "array",
@@ -8784,6 +8901,18 @@ const docTemplate = `{
"description": "PaidAt 支付时间RFC3339。", "description": "PaidAt 支付时间RFC3339。",
"type": "string" "type": "string"
}, },
"reconcile_note": {
"description": "ReconcileNote 对账说明。",
"type": "string"
},
"reconciled_at": {
"description": "ReconciledAt 对账时间RFC3339。",
"type": "string"
},
"reconciled_by": {
"description": "ReconciledBy 对账操作者ID。",
"type": "integer"
},
"refunded_at": { "refunded_at": {
"description": "RefundedAt 退款时间RFC3339。", "description": "RefundedAt 退款时间RFC3339。",
"type": "string" "type": "string"
@@ -8845,6 +8974,19 @@ const docTemplate = `{
} }
} }
}, },
"dto.SuperOrderReconcileForm": {
"type": "object",
"properties": {
"is_reconciled": {
"description": "IsReconciled 是否完成对账。",
"type": "boolean"
},
"note": {
"description": "Note 对账说明(可选)。",
"type": "string"
}
}
},
"dto.SuperOrderRefundForm": { "dto.SuperOrderRefundForm": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -1343,6 +1343,90 @@
} }
} }
}, },
"/super/v1/orders/{id}/flag": {
"post": {
"description": "Flag or unflag an order as problematic",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Order"
],
"summary": "Flag order",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Order ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Flag form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperOrderFlagForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/orders/{id}/reconcile": {
"post": {
"description": "Mark or unmark order reconciliation status",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Order"
],
"summary": "Reconcile order",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Order ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Reconcile form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperOrderReconcileForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/orders/{id}/refund": { "/super/v1/orders/{id}/refund": {
"post": { "post": {
"description": "Refund order", "description": "Refund order",
@@ -8728,6 +8812,19 @@
} }
} }
}, },
"dto.SuperOrderFlagForm": {
"type": "object",
"properties": {
"is_flagged": {
"description": "IsFlagged 是否标记为问题订单。",
"type": "boolean"
},
"reason": {
"description": "Reason 标记原因(标记为问题时必填)。",
"type": "string"
}
}
},
"dto.SuperOrderItem": { "dto.SuperOrderItem": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -8763,10 +8860,30 @@
} }
] ]
}, },
"flag_reason": {
"description": "FlagReason 问题标记原因。",
"type": "string"
},
"flagged_at": {
"description": "FlaggedAt 标记时间RFC3339。",
"type": "string"
},
"flagged_by": {
"description": "FlaggedBy 标记操作者ID。",
"type": "integer"
},
"id": { "id": {
"description": "ID 订单ID。", "description": "ID 订单ID。",
"type": "integer" "type": "integer"
}, },
"is_flagged": {
"description": "IsFlagged 是否标记为问题订单。",
"type": "boolean"
},
"is_reconciled": {
"description": "IsReconciled 是否完成对账。",
"type": "boolean"
},
"items": { "items": {
"description": "Items 订单明细行,用于展示具体内容与金额拆分。", "description": "Items 订单明细行,用于展示具体内容与金额拆分。",
"type": "array", "type": "array",
@@ -8778,6 +8895,18 @@
"description": "PaidAt 支付时间RFC3339。", "description": "PaidAt 支付时间RFC3339。",
"type": "string" "type": "string"
}, },
"reconcile_note": {
"description": "ReconcileNote 对账说明。",
"type": "string"
},
"reconciled_at": {
"description": "ReconciledAt 对账时间RFC3339。",
"type": "string"
},
"reconciled_by": {
"description": "ReconciledBy 对账操作者ID。",
"type": "integer"
},
"refunded_at": { "refunded_at": {
"description": "RefundedAt 退款时间RFC3339。", "description": "RefundedAt 退款时间RFC3339。",
"type": "string" "type": "string"
@@ -8839,6 +8968,19 @@
} }
} }
}, },
"dto.SuperOrderReconcileForm": {
"type": "object",
"properties": {
"is_reconciled": {
"description": "IsReconciled 是否完成对账。",
"type": "boolean"
},
"note": {
"description": "Note 对账说明(可选)。",
"type": "string"
}
}
},
"dto.SuperOrderRefundForm": { "dto.SuperOrderRefundForm": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -1936,6 +1936,15 @@ definitions:
- $ref: '#/definitions/dto.OrderTenantLite' - $ref: '#/definitions/dto.OrderTenantLite'
description: Tenant 订单所属租户信息。 description: Tenant 订单所属租户信息。
type: object type: object
dto.SuperOrderFlagForm:
properties:
is_flagged:
description: IsFlagged 是否标记为问题订单。
type: boolean
reason:
description: Reason 标记原因(标记为问题时必填)。
type: string
type: object
dto.SuperOrderItem: dto.SuperOrderItem:
properties: properties:
amount_discount: amount_discount:
@@ -1958,9 +1967,24 @@ definitions:
allOf: allOf:
- $ref: '#/definitions/consts.Currency' - $ref: '#/definitions/consts.Currency'
description: Currency 币种。 description: Currency 币种。
flag_reason:
description: FlagReason 问题标记原因。
type: string
flagged_at:
description: FlaggedAt 标记时间RFC3339
type: string
flagged_by:
description: FlaggedBy 标记操作者ID。
type: integer
id: id:
description: ID 订单ID。 description: ID 订单ID。
type: integer type: integer
is_flagged:
description: IsFlagged 是否标记为问题订单。
type: boolean
is_reconciled:
description: IsReconciled 是否完成对账。
type: boolean
items: items:
description: Items 订单明细行,用于展示具体内容与金额拆分。 description: Items 订单明细行,用于展示具体内容与金额拆分。
items: items:
@@ -1969,6 +1993,15 @@ definitions:
paid_at: paid_at:
description: PaidAt 支付时间RFC3339 description: PaidAt 支付时间RFC3339
type: string type: string
reconcile_note:
description: ReconcileNote 对账说明。
type: string
reconciled_at:
description: ReconciledAt 对账时间RFC3339
type: string
reconciled_by:
description: ReconciledBy 对账操作者ID。
type: integer
refunded_at: refunded_at:
description: RefundedAt 退款时间RFC3339 description: RefundedAt 退款时间RFC3339
type: string type: string
@@ -2007,6 +2040,15 @@ definitions:
snapshot: snapshot:
description: Snapshot 明细快照,用于展示内容标题等历史信息。 description: Snapshot 明细快照,用于展示内容标题等历史信息。
type: object type: object
dto.SuperOrderReconcileForm:
properties:
is_reconciled:
description: IsReconciled 是否完成对账。
type: boolean
note:
description: Note 对账说明(可选)。
type: string
type: object
dto.SuperOrderRefundForm: dto.SuperOrderRefundForm:
properties: properties:
force: force:
@@ -4028,6 +4070,62 @@ paths:
summary: Get order summary: Get order
tags: tags:
- Order - Order
/super/v1/orders/{id}/flag:
post:
consumes:
- application/json
description: Flag or unflag an order as problematic
parameters:
- description: Order ID
format: int64
in: path
name: id
required: true
type: integer
- description: Flag form
in: body
name: form
required: true
schema:
$ref: '#/definitions/dto.SuperOrderFlagForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Flag order
tags:
- Order
/super/v1/orders/{id}/reconcile:
post:
consumes:
- application/json
description: Mark or unmark order reconciliation status
parameters:
- description: Order ID
format: int64
in: path
name: id
required: true
type: integer
- description: Reconcile form
in: body
name: form
required: true
schema:
$ref: '#/definitions/dto.SuperOrderReconcileForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Reconcile order
tags:
- Order
/super/v1/orders/{id}/refund: /super/v1/orders/{id}/refund:
post: post:
consumes: consumes:

View File

@@ -47,8 +47,8 @@
### 2.8 订单与退款 `/superadmin/orders` ### 2.8 订单与退款 `/superadmin/orders`
- 状态:**已完成** - 状态:**已完成**
- 已有:列表、详情、退款申请。 - 已有:列表、详情、退款申请、问题订单标记、对账辅助
- 缺口:问题订单标记/支付对账等运营增强能力 - 缺口:无显著功能缺口
### 2.9 创作者与成员审核 `/superadmin/creators` ### 2.9 创作者与成员审核 `/superadmin/creators`
- 状态:**部分完成** - 状态:**部分完成**
@@ -92,6 +92,5 @@
## 4) 建议的下一步(按优先级) ## 4) 建议的下一步(按优先级)
1. **订单运营补强**:问题订单标记、支付对账辅助能力 1. **内容批量处置扩展**:补齐内容/举报的批量下架、封禁与归档处理
2. **内容批量处置扩展**:补齐内容/举报的批量下架、封禁与归档处理 2. **创作者治理补强**:结算账户审批流、提现审核联动流程
3. **创作者治理补强**:结算账户审批流、提现审核联动流程。

View File

@@ -20,6 +20,8 @@ export const OrderService = {
content_title, content_title,
type, type,
status, status,
is_flagged,
is_reconciled,
created_at_from, created_at_from,
created_at_to, created_at_to,
paid_at_from, paid_at_from,
@@ -49,6 +51,8 @@ export const OrderService = {
content_title, content_title,
type, type,
status, status,
is_flagged,
is_reconciled,
created_at_from: iso(created_at_from), created_at_from: iso(created_at_from),
created_at_to: iso(created_at_to), created_at_to: iso(created_at_to),
paid_at_from: iso(paid_at_from), paid_at_from: iso(paid_at_from),
@@ -86,5 +90,25 @@ export const OrderService = {
idempotency_key idempotency_key
} }
}); });
},
async flagOrder(orderID, { is_flagged, reason } = {}) {
if (!orderID) throw new Error('orderID is required');
return requestJson(`/super/v1/orders/${orderID}/flag`, {
method: 'POST',
body: {
is_flagged: Boolean(is_flagged),
reason
}
});
},
async reconcileOrder(orderID, { is_reconciled, note } = {}) {
if (!orderID) throw new Error('orderID is required');
return requestJson(`/super/v1/orders/${orderID}/reconcile`, {
method: 'POST',
body: {
is_reconciled: Boolean(is_reconciled),
note
}
});
} }
}; };

View File

@@ -17,6 +17,16 @@ const refundLoading = ref(false);
const refundForce = ref(false); const refundForce = ref(false);
const refundReason = ref(''); const refundReason = ref('');
const flagDialogVisible = ref(false);
const flagLoading = ref(false);
const flagValue = ref(false);
const flagReason = ref('');
const reconcileDialogVisible = ref(false);
const reconcileLoading = ref(false);
const reconcileValue = ref(false);
const reconcileNote = ref('');
function formatDate(value) { function formatDate(value) {
if (!value) return '-'; if (!value) return '-';
if (String(value).startsWith('0001-01-01')) return '-'; if (String(value).startsWith('0001-01-01')) return '-';
@@ -46,6 +56,14 @@ function getOrderStatusSeverity(value) {
} }
} }
function getFlagSeverity(value) {
return value ? 'danger' : 'secondary';
}
function getReconcileSeverity(value) {
return value ? 'success' : 'secondary';
}
function safeJson(value) { function safeJson(value) {
try { try {
return JSON.stringify(value ?? null, null, 2); return JSON.stringify(value ?? null, null, 2);
@@ -89,6 +107,66 @@ async function confirmRefund() {
} }
} }
function openFlagDialog() {
const order = detail.value?.order;
if (!order) return;
flagValue.value = Boolean(order.is_flagged);
flagReason.value = order.flag_reason || '';
flagDialogVisible.value = true;
}
async function confirmFlag() {
const order = detail.value?.order;
if (!order?.id) return;
if (flagValue.value && !flagReason.value.trim()) {
toast.add({ severity: 'warn', summary: '请填写原因', detail: '标记为问题订单时需要填写原因', life: 3000 });
return;
}
flagLoading.value = true;
try {
await OrderService.flagOrder(order.id, {
is_flagged: flagValue.value,
reason: flagValue.value ? flagReason.value : ''
});
toast.add({ severity: 'success', summary: '已更新标记', detail: `订单ID: ${order.id}`, life: 3000 });
flagDialogVisible.value = false;
await loadDetail();
} catch (error) {
toast.add({ severity: 'error', summary: '操作失败', detail: error?.message || '无法更新问题标记', life: 4000 });
} finally {
flagLoading.value = false;
}
}
function openReconcileDialog() {
const order = detail.value?.order;
if (!order) return;
reconcileValue.value = Boolean(order.is_reconciled);
reconcileNote.value = order.reconcile_note || '';
reconcileDialogVisible.value = true;
}
async function confirmReconcile() {
const order = detail.value?.order;
if (!order?.id) return;
reconcileLoading.value = true;
try {
await OrderService.reconcileOrder(order.id, {
is_reconciled: reconcileValue.value,
note: reconcileValue.value ? reconcileNote.value : ''
});
toast.add({ severity: 'success', summary: '已更新对账', detail: `订单ID: ${order.id}`, life: 3000 });
reconcileDialogVisible.value = false;
await loadDetail();
} catch (error) {
toast.add({ severity: 'error', summary: '操作失败', detail: error?.message || '无法更新对账状态', life: 4000 });
} finally {
reconcileLoading.value = false;
}
}
watch( watch(
() => orderID.value, () => orderID.value,
() => loadDetail(), () => loadDetail(),
@@ -104,6 +182,8 @@ watch(
<span class="text-muted-color">OrderID: {{ orderID || '-' }}</span> <span class="text-muted-color">OrderID: {{ orderID || '-' }}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Button :label="detail?.order?.is_flagged ? '取消标记' : '标记问题'" icon="pi pi-flag" text @click="openFlagDialog" :disabled="loading" />
<Button :label="detail?.order?.is_reconciled ? '撤销对账' : '完成对账'" icon="pi pi-check-circle" text @click="openReconcileDialog" :disabled="loading" />
<Button label="退款" icon="pi pi-replay" severity="danger" @click="openRefundDialog" :disabled="detail?.order?.status !== 'paid' || loading" /> <Button label="退款" icon="pi pi-replay" severity="danger" @click="openRefundDialog" :disabled="detail?.order?.status !== 'paid' || loading" />
</div> </div>
</div> </div>
@@ -140,6 +220,26 @@ watch(
<div class="text-sm text-muted-color">支付时间</div> <div class="text-sm text-muted-color">支付时间</div>
<div class="font-medium">{{ formatDate(detail?.order?.paid_at) }}</div> <div class="font-medium">{{ formatDate(detail?.order?.paid_at) }}</div>
</div> </div>
<div class="col-span-12 md:col-span-3">
<div class="text-sm text-muted-color">问题标记</div>
<Tag :value="detail?.order?.is_flagged ? '已标记' : '未标记'" :severity="getFlagSeverity(detail?.order?.is_flagged)" />
<div v-if="detail?.order?.is_flagged" class="text-xs text-muted-color mt-1">
{{ detail?.order?.flag_reason || '无原因' }}
</div>
<div v-if="detail?.order?.is_flagged" class="text-xs text-muted-color">
{{ formatDate(detail?.order?.flagged_at) }}
</div>
</div>
<div class="col-span-12 md:col-span-3">
<div class="text-sm text-muted-color">对账状态</div>
<Tag :value="detail?.order?.is_reconciled ? '已对账' : '未对账'" :severity="getReconcileSeverity(detail?.order?.is_reconciled)" />
<div v-if="detail?.order?.is_reconciled" class="text-xs text-muted-color mt-1">
{{ detail?.order?.reconcile_note || '无说明' }}
</div>
<div v-if="detail?.order?.is_reconciled" class="text-xs text-muted-color">
{{ formatDate(detail?.order?.reconciled_at) }}
</div>
</div>
</div> </div>
<div class="grid grid-cols-12 gap-3"> <div class="grid grid-cols-12 gap-3">
@@ -191,4 +291,52 @@ watch(
<Button label="确认退款" icon="pi pi-check" severity="danger" @click="confirmRefund" :loading="refundLoading" :disabled="detail?.order?.status !== 'paid'" /> <Button label="确认退款" icon="pi pi-check" severity="danger" @click="confirmRefund" :loading="refundLoading" :disabled="detail?.order?.status !== 'paid'" />
</template> </template>
</Dialog> </Dialog>
<Dialog v-model:visible="flagDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">
<span class="font-medium">问题订单标记</span>
<span class="text-muted-color truncate max-w-[280px]">{{ orderID ? `#${orderID}` : '' }}</span>
</div>
</template>
<div class="flex flex-col gap-4">
<div class="text-sm text-muted-color">标记后将用于运营复核与对账追踪可随时取消</div>
<div class="flex items-center gap-2">
<Checkbox v-model="flagValue" inputId="flagValueDetail" binary />
<label for="flagValueDetail" class="cursor-pointer">标记为问题订单</label>
</div>
<div>
<label class="block font-medium mb-2">标记原因</label>
<InputText v-model="flagReason" placeholder="标记为问题时必填" class="w-full" :disabled="!flagValue" />
</div>
</div>
<template #footer>
<Button label="取消" icon="pi pi-times" text @click="flagDialogVisible = false" :disabled="flagLoading" />
<Button label="确认" icon="pi pi-check" @click="confirmFlag" :loading="flagLoading" :disabled="flagValue && !flagReason.trim()" />
</template>
</Dialog>
<Dialog v-model:visible="reconcileDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">
<span class="font-medium">订单对账</span>
<span class="text-muted-color truncate max-w-[280px]">{{ orderID ? `#${orderID}` : '' }}</span>
</div>
</template>
<div class="flex flex-col gap-4">
<div class="text-sm text-muted-color">用于记录人工对账结果与备注</div>
<div class="flex items-center gap-2">
<Checkbox v-model="reconcileValue" inputId="reconcileValueDetail" binary />
<label for="reconcileValueDetail" class="cursor-pointer">完成对账</label>
</div>
<div>
<label class="block font-medium mb-2">对账说明</label>
<InputText v-model="reconcileNote" placeholder="可选" class="w-full" :disabled="!reconcileValue" />
</div>
</div>
<template #footer>
<Button label="取消" icon="pi pi-times" text @click="reconcileDialogVisible = false" :disabled="reconcileLoading" />
<Button label="确认" icon="pi pi-check" @click="confirmReconcile" :loading="reconcileLoading" />
</template>
</Dialog>
</template> </template>

View File

@@ -32,6 +32,8 @@ const contentID = ref(null);
const contentTitle = ref(''); const contentTitle = ref('');
const status = ref(''); const status = ref('');
const type = ref(''); const type = ref('');
const isFlagged = ref(null);
const isReconciled = ref(null);
const createdAtFrom = ref(null); const createdAtFrom = ref(null);
const createdAtTo = ref(null); const createdAtTo = ref(null);
const paidAtFrom = ref(null); const paidAtFrom = ref(null);
@@ -56,6 +58,12 @@ const typeOptions = [
{ label: 'content_purchase', value: 'content_purchase' } { label: 'content_purchase', value: 'content_purchase' }
]; ];
const booleanOptions = [
{ label: '全部', value: null },
{ label: '是', value: true },
{ label: '否', value: false }
];
function getQueryValue(value) { function getQueryValue(value) {
if (Array.isArray(value)) return value[0]; if (Array.isArray(value)) return value[0];
return value ?? null; return value ?? null;
@@ -74,6 +82,15 @@ function parseDate(value) {
return date; return date;
} }
function parseBool(value) {
if (value === null || value === undefined || value === '') return null;
if (typeof value === 'boolean') return value;
const text = String(value).toLowerCase();
if (text === 'true' || text === '1') return true;
if (text === 'false' || text === '0') return false;
return null;
}
function formatDate(value) { function formatDate(value) {
if (!value) return '-'; if (!value) return '-';
if (String(value).startsWith('0001-01-01')) return '-'; if (String(value).startsWith('0001-01-01')) return '-';
@@ -93,6 +110,8 @@ function resetFilters() {
contentTitle.value = ''; contentTitle.value = '';
status.value = ''; status.value = '';
type.value = ''; type.value = '';
isFlagged.value = null;
isReconciled.value = null;
createdAtFrom.value = null; createdAtFrom.value = null;
createdAtTo.value = null; createdAtTo.value = null;
paidAtFrom.value = null; paidAtFrom.value = null;
@@ -121,6 +140,11 @@ function applyRouteQuery(query) {
if (statusValue !== null) status.value = String(statusValue); if (statusValue !== null) status.value = String(statusValue);
if (typeValue !== null) type.value = String(typeValue); if (typeValue !== null) type.value = String(typeValue);
const flaggedValue = parseBool(getQueryValue(query?.is_flagged));
const reconciledValue = parseBool(getQueryValue(query?.is_reconciled));
if (flaggedValue !== null) isFlagged.value = flaggedValue;
if (reconciledValue !== null) isReconciled.value = reconciledValue;
const createdFromValue = getQueryValue(query?.created_at_from); const createdFromValue = getQueryValue(query?.created_at_from);
const createdToValue = getQueryValue(query?.created_at_to); const createdToValue = getQueryValue(query?.created_at_to);
const paidFromValue = getQueryValue(query?.paid_at_from); const paidFromValue = getQueryValue(query?.paid_at_from);
@@ -153,6 +177,14 @@ function getOrderStatusSeverity(value) {
} }
} }
function getFlagSeverity(value) {
return value ? 'danger' : 'secondary';
}
function getReconcileSeverity(value) {
return value ? 'success' : 'secondary';
}
async function loadOrders() { async function loadOrders() {
loading.value = true; loading.value = true;
try { try {
@@ -169,6 +201,8 @@ async function loadOrders() {
content_title: contentTitle.value, content_title: contentTitle.value,
status: status.value, status: status.value,
type: type.value, type: type.value,
is_flagged: isFlagged.value === null ? undefined : isFlagged.value,
is_reconciled: isReconciled.value === null ? undefined : isReconciled.value,
created_at_from: createdAtFrom.value || undefined, created_at_from: createdAtFrom.value || undefined,
created_at_to: createdAtTo.value || undefined, created_at_to: createdAtTo.value || undefined,
paid_at_from: paidAtFrom.value || undefined, paid_at_from: paidAtFrom.value || undefined,
@@ -187,6 +221,18 @@ async function loadOrders() {
} }
} }
const flagDialogVisible = ref(false);
const flagLoading = ref(false);
const flagOrder = ref(null);
const flagValue = ref(false);
const flagReason = ref('');
const reconcileDialogVisible = ref(false);
const reconcileLoading = ref(false);
const reconcileOrder = ref(null);
const reconcileValue = ref(false);
const reconcileNote = ref('');
function openRefundDialog(order) { function openRefundDialog(order) {
refundOrder.value = order; refundOrder.value = order;
refundDialogVisible.value = true; refundDialogVisible.value = true;
@@ -211,6 +257,64 @@ async function confirmRefund() {
} }
} }
function openFlagDialog(order) {
flagOrder.value = order;
flagValue.value = Boolean(order?.is_flagged);
flagReason.value = order?.flag_reason || '';
flagDialogVisible.value = true;
}
async function confirmFlag() {
const id = flagOrder.value?.id;
if (!id) return;
if (flagValue.value && !flagReason.value.trim()) {
toast.add({ severity: 'warn', summary: '请填写原因', detail: '标记为问题订单时需要填写原因', life: 3000 });
return;
}
flagLoading.value = true;
try {
await OrderService.flagOrder(id, {
is_flagged: flagValue.value,
reason: flagValue.value ? flagReason.value : ''
});
toast.add({ severity: 'success', summary: '已更新标记', detail: `订单ID: ${id}`, life: 3000 });
flagDialogVisible.value = false;
await loadOrders();
} catch (error) {
toast.add({ severity: 'error', summary: '操作失败', detail: error?.message || '无法更新问题标记', life: 4000 });
} finally {
flagLoading.value = false;
}
}
function openReconcileDialog(order) {
reconcileOrder.value = order;
reconcileValue.value = Boolean(order?.is_reconciled);
reconcileNote.value = order?.reconcile_note || '';
reconcileDialogVisible.value = true;
}
async function confirmReconcile() {
const id = reconcileOrder.value?.id;
if (!id) return;
reconcileLoading.value = true;
try {
await OrderService.reconcileOrder(id, {
is_reconciled: reconcileValue.value,
note: reconcileValue.value ? reconcileNote.value : ''
});
toast.add({ severity: 'success', summary: '已更新对账', detail: `订单ID: ${id}`, life: 3000 });
reconcileDialogVisible.value = false;
await loadOrders();
} catch (error) {
toast.add({ severity: 'error', summary: '操作失败', detail: error?.message || '无法更新对账状态', life: 4000 });
} finally {
reconcileLoading.value = false;
}
}
function onSearch() { function onSearch() {
page.value = 1; page.value = 1;
loadOrders(); loadOrders();
@@ -289,6 +393,12 @@ watch(
<SearchField label="类型"> <SearchField label="类型">
<Select v-model="type" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" /> <Select v-model="type" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
</SearchField> </SearchField>
<SearchField label="问题标记">
<Select v-model="isFlagged" :options="booleanOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
</SearchField>
<SearchField label="对账状态">
<Select v-model="isReconciled" :options="booleanOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
</SearchField>
<SearchField label="创建时间 From"> <SearchField label="创建时间 From">
<DatePicker v-model="createdAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" /> <DatePicker v-model="createdAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
</SearchField> </SearchField>
@@ -358,6 +468,24 @@ watch(
<Tag :value="data?.status_description || data?.status || '-'" :severity="getOrderStatusSeverity(data?.status)" /> <Tag :value="data?.status_description || data?.status || '-'" :severity="getOrderStatusSeverity(data?.status)" />
</template> </template>
</Column> </Column>
<Column header="问题标记" style="min-width: 12rem">
<template #body="{ data }">
<div class="flex flex-col">
<Tag :value="data?.is_flagged ? '已标记' : '未标记'" :severity="getFlagSeverity(data?.is_flagged)" />
<span v-if="data?.is_flagged && data?.flag_reason" class="text-xs text-muted-color mt-1">{{ data.flag_reason }}</span>
<span v-if="data?.is_flagged && data?.flagged_at" class="text-xs text-muted-color">{{ formatDate(data.flagged_at) }}</span>
</div>
</template>
</Column>
<Column header="对账状态" style="min-width: 12rem">
<template #body="{ data }">
<div class="flex flex-col">
<Tag :value="data?.is_reconciled ? '已对账' : '未对账'" :severity="getReconcileSeverity(data?.is_reconciled)" />
<span v-if="data?.is_reconciled && data?.reconcile_note" class="text-xs text-muted-color mt-1">{{ data.reconcile_note }}</span>
<span v-if="data?.is_reconciled && data?.reconciled_at" class="text-xs text-muted-color">{{ formatDate(data.reconciled_at) }}</span>
</div>
</template>
</Column>
<Column field="amount_paid" header="实付" sortable style="min-width: 10rem"> <Column field="amount_paid" header="实付" sortable style="min-width: 10rem">
<template #body="{ data }"> <template #body="{ data }">
{{ formatCny(data.amount_paid) }} {{ formatCny(data.amount_paid) }}
@@ -390,7 +518,11 @@ watch(
</Column> </Column>
<Column header="操作" style="min-width: 10rem"> <Column header="操作" style="min-width: 10rem">
<template #body="{ data }"> <template #body="{ data }">
<Button label="退款" icon="pi pi-replay" text size="small" class="p-0" :disabled="data?.status !== 'paid'" @click="openRefundDialog(data)" /> <div class="flex flex-col gap-1">
<Button label="退款" icon="pi pi-replay" text size="small" class="p-0 justify-start" :disabled="data?.status !== 'paid'" @click="openRefundDialog(data)" />
<Button :label="data?.is_flagged ? '取消标记' : '标记问题'" icon="pi pi-flag" text size="small" class="p-0 justify-start" @click="openFlagDialog(data)" />
<Button :label="data?.is_reconciled ? '撤销对账' : '完成对账'" icon="pi pi-check-circle" text size="small" class="p-0 justify-start" @click="openReconcileDialog(data)" />
</div>
</template> </template>
</Column> </Column>
</DataTable> </DataTable>
@@ -421,4 +553,56 @@ watch(
<Button label="确认退款" icon="pi pi-check" severity="danger" @click="confirmRefund" :loading="refundLoading" :disabled="refundOrder?.status !== 'paid'" /> <Button label="确认退款" icon="pi pi-check" severity="danger" @click="confirmRefund" :loading="refundLoading" :disabled="refundOrder?.status !== 'paid'" />
</template> </template>
</Dialog> </Dialog>
<Dialog v-model:visible="flagDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">
<span class="font-medium">问题订单标记</span>
<span class="text-muted-color truncate max-w-[280px]">
{{ flagOrder?.id ? `#${flagOrder.id}` : '' }}
</span>
</div>
</template>
<div class="flex flex-col gap-4">
<div class="text-sm text-muted-color">标记后将用于运营复核与对账追踪可随时取消</div>
<div class="flex items-center gap-2">
<Checkbox v-model="flagValue" inputId="flagValue" binary />
<label for="flagValue" class="cursor-pointer">标记为问题订单</label>
</div>
<div>
<label class="block font-medium mb-2">标记原因</label>
<InputText v-model="flagReason" placeholder="必填" class="w-full" :disabled="!flagValue" />
</div>
</div>
<template #footer>
<Button label="取消" icon="pi pi-times" text @click="flagDialogVisible = false" :disabled="flagLoading" />
<Button label="确认" icon="pi pi-check" @click="confirmFlag" :loading="flagLoading" :disabled="flagValue && !flagReason.trim()" />
</template>
</Dialog>
<Dialog v-model:visible="reconcileDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">
<span class="font-medium">订单对账</span>
<span class="text-muted-color truncate max-w-[280px]">
{{ reconcileOrder?.id ? `#${reconcileOrder.id}` : '' }}
</span>
</div>
</template>
<div class="flex flex-col gap-4">
<div class="text-sm text-muted-color">对账用于确认支付状态与对账结果支持撤销</div>
<div class="flex items-center gap-2">
<Checkbox v-model="reconcileValue" inputId="reconcileValue" binary />
<label for="reconcileValue" class="cursor-pointer">完成对账</label>
</div>
<div>
<label class="block font-medium mb-2">对账说明</label>
<InputText v-model="reconcileNote" placeholder="可选" class="w-full" :disabled="!reconcileValue" />
</div>
</div>
<template #footer>
<Button label="取消" icon="pi pi-times" text @click="reconcileDialogVisible = false" :disabled="reconcileLoading" />
<Button label="确认" icon="pi pi-check" @click="confirmReconcile" :loading="reconcileLoading" />
</template>
</Dialog>
</template> </template>