feat: add order governance flags and reconciliation
This commit is contained in:
@@ -165,6 +165,10 @@ type SuperOrderListFilter struct {
|
||||
Type *consts.OrderType `query:"type"`
|
||||
// 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 *string `query:"created_at_from"`
|
||||
// CreatedAtTo 创建时间结束(RFC3339)。
|
||||
@@ -549,6 +553,22 @@ type SuperOrderItem struct {
|
||||
PaidAt string `json:"paid_at"`
|
||||
// RefundedAt 退款时间(RFC3339)。
|
||||
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 string `json:"created_at"`
|
||||
// UpdatedAt 更新时间(RFC3339)。
|
||||
@@ -621,6 +641,20 @@ type SuperOrderRefundForm struct {
|
||||
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
|
||||
type AdminContentItem struct {
|
||||
// Content 内容摘要信息。
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
dto "quyun/v2/app/http/super/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/database/models"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// @Router /super/v1/orders/:id<int>/refund [post]
|
||||
|
||||
@@ -267,6 +267,20 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
router.Get("/super/v1/orders/statistics"[len(r.Path()):], DataFunc0(
|
||||
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")
|
||||
router.Post("/super/v1/orders/:id<int>/refund"[len(r.Path()):], Func2(
|
||||
r.orders.Refund,
|
||||
|
||||
@@ -4529,6 +4529,12 @@ func (s *super) ListOrders(ctx context.Context, filter *super_dto.SuperOrderList
|
||||
if filter.Status != nil && *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 {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).First()
|
||||
if err != nil {
|
||||
@@ -4939,6 +5079,12 @@ func (s *super) toSuperOrderItem(o *models.Order, tenant *models.Tenant, buyer *
|
||||
AmountDiscount: o.AmountDiscount,
|
||||
AmountPaid: o.AmountPaid,
|
||||
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),
|
||||
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() {
|
||||
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 {
|
||||
item.Tenant = &super_dto.OrderTenantLite{
|
||||
|
||||
@@ -2,10 +2,12 @@ package services
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"quyun/v2/app/commands/testx"
|
||||
"quyun/v2/app/errorx"
|
||||
super_dto "quyun/v2/app/http/super/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/database"
|
||||
@@ -597,3 +599,92 @@ func (s *SuperTestSuite) Test_ContentReview() {
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -40,9 +40,9 @@ type Content struct {
|
||||
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
|
||||
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"`
|
||||
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
|
||||
|
||||
@@ -46,6 +46,12 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery {
|
||||
_contentQuery.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||
_contentQuery.Key = field.NewString(tableName, "key")
|
||||
_contentQuery.IsPinned = field.NewBool(tableName, "is_pinned")
|
||||
_contentQuery.Comments = contentQueryHasManyComments{
|
||||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
RelationField: field.NewRelation("Comments", "Comment"),
|
||||
}
|
||||
|
||||
_contentQuery.Author = contentQueryBelongsToAuthor{
|
||||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
@@ -58,12 +64,6 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery {
|
||||
RelationField: field.NewRelation("ContentAssets", "ContentAsset"),
|
||||
}
|
||||
|
||||
_contentQuery.Comments = contentQueryHasManyComments{
|
||||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
RelationField: field.NewRelation("Comments", "Comment"),
|
||||
}
|
||||
|
||||
_contentQuery.fillFieldMap()
|
||||
|
||||
return _contentQuery
|
||||
@@ -94,12 +94,12 @@ type contentQuery struct {
|
||||
DeletedAt field.Field
|
||||
Key field.String // Musical key/tone
|
||||
IsPinned field.Bool // Whether content is pinned/featured
|
||||
Author contentQueryBelongsToAuthor
|
||||
Comments contentQueryHasManyComments
|
||||
|
||||
Author contentQueryBelongsToAuthor
|
||||
|
||||
ContentAssets contentQueryHasManyContentAssets
|
||||
|
||||
Comments contentQueryHasManyComments
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
||||
@@ -195,23 +195,104 @@ func (c *contentQuery) fillFieldMap() {
|
||||
|
||||
func (c contentQuery) clone(db *gorm.DB) contentQuery {
|
||||
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.Statement.ConnPool = db.Statement.ConnPool
|
||||
c.ContentAssets.db = db.Session(&gorm.Session{Initialized: true})
|
||||
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
|
||||
}
|
||||
|
||||
func (c contentQuery) replaceDB(db *gorm.DB) contentQuery {
|
||||
c.contentQueryDo.ReplaceDB(db)
|
||||
c.Comments.db = db.Session(&gorm.Session{})
|
||||
c.Author.db = db.Session(&gorm.Session{})
|
||||
c.ContentAssets.db = db.Session(&gorm.Session{})
|
||||
c.Comments.db = db.Session(&gorm.Session{})
|
||||
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 {
|
||||
db *gorm.DB
|
||||
|
||||
@@ -374,87 +455,6 @@ func (a contentQueryHasManyContentAssetsTx) Unscoped() *contentQueryHasManyConte
|
||||
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 }
|
||||
|
||||
func (c contentQueryDo) Debug() *contentQueryDo {
|
||||
|
||||
@@ -37,7 +37,15 @@ type Order struct {
|
||||
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"`
|
||||
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
|
||||
|
||||
@@ -44,6 +44,14 @@ func newOrder(db *gorm.DB, opts ...gen.DOOption) orderQuery {
|
||||
_orderQuery.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_orderQuery.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_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()
|
||||
|
||||
@@ -72,7 +80,15 @@ type orderQuery struct {
|
||||
RefundReason field.String
|
||||
CreatedAt 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
|
||||
}
|
||||
@@ -108,6 +124,14 @@ func (o *orderQuery) updateTableName(table string) *orderQuery {
|
||||
o.CreatedAt = field.NewTime(table, "created_at")
|
||||
o.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
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()
|
||||
|
||||
@@ -138,7 +162,7 @@ func (o *orderQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
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["tenant_id"] = o.TenantID
|
||||
o.fieldMap["user_id"] = o.UserID
|
||||
@@ -158,6 +182,14 @@ func (o *orderQuery) fillFieldMap() {
|
||||
o.fieldMap["created_at"] = o.CreatedAt
|
||||
o.fieldMap["updated_at"] = o.UpdatedAt
|
||||
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 {
|
||||
|
||||
@@ -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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"description": "ID 订单ID。",
|
||||
"type": "integer"
|
||||
},
|
||||
"is_flagged": {
|
||||
"description": "IsFlagged 是否标记为问题订单。",
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_reconciled": {
|
||||
"description": "IsReconciled 是否完成对账。",
|
||||
"type": "boolean"
|
||||
},
|
||||
"items": {
|
||||
"description": "Items 订单明细行,用于展示具体内容与金额拆分。",
|
||||
"type": "array",
|
||||
@@ -8784,6 +8901,18 @@ const docTemplate = `{
|
||||
"description": "PaidAt 支付时间(RFC3339)。",
|
||||
"type": "string"
|
||||
},
|
||||
"reconcile_note": {
|
||||
"description": "ReconcileNote 对账说明。",
|
||||
"type": "string"
|
||||
},
|
||||
"reconciled_at": {
|
||||
"description": "ReconciledAt 对账时间(RFC3339)。",
|
||||
"type": "string"
|
||||
},
|
||||
"reconciled_by": {
|
||||
"description": "ReconciledBy 对账操作者ID。",
|
||||
"type": "integer"
|
||||
},
|
||||
"refunded_at": {
|
||||
"description": "RefundedAt 退款时间(RFC3339)。",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"description": "ID 订单ID。",
|
||||
"type": "integer"
|
||||
},
|
||||
"is_flagged": {
|
||||
"description": "IsFlagged 是否标记为问题订单。",
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_reconciled": {
|
||||
"description": "IsReconciled 是否完成对账。",
|
||||
"type": "boolean"
|
||||
},
|
||||
"items": {
|
||||
"description": "Items 订单明细行,用于展示具体内容与金额拆分。",
|
||||
"type": "array",
|
||||
@@ -8778,6 +8895,18 @@
|
||||
"description": "PaidAt 支付时间(RFC3339)。",
|
||||
"type": "string"
|
||||
},
|
||||
"reconcile_note": {
|
||||
"description": "ReconcileNote 对账说明。",
|
||||
"type": "string"
|
||||
},
|
||||
"reconciled_at": {
|
||||
"description": "ReconciledAt 对账时间(RFC3339)。",
|
||||
"type": "string"
|
||||
},
|
||||
"reconciled_by": {
|
||||
"description": "ReconciledBy 对账操作者ID。",
|
||||
"type": "integer"
|
||||
},
|
||||
"refunded_at": {
|
||||
"description": "RefundedAt 退款时间(RFC3339)。",
|
||||
"type": "string"
|
||||
@@ -8839,6 +8968,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SuperOrderReconcileForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"is_reconciled": {
|
||||
"description": "IsReconciled 是否完成对账。",
|
||||
"type": "boolean"
|
||||
},
|
||||
"note": {
|
||||
"description": "Note 对账说明(可选)。",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SuperOrderRefundForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1936,6 +1936,15 @@ definitions:
|
||||
- $ref: '#/definitions/dto.OrderTenantLite'
|
||||
description: Tenant 订单所属租户信息。
|
||||
type: object
|
||||
dto.SuperOrderFlagForm:
|
||||
properties:
|
||||
is_flagged:
|
||||
description: IsFlagged 是否标记为问题订单。
|
||||
type: boolean
|
||||
reason:
|
||||
description: Reason 标记原因(标记为问题时必填)。
|
||||
type: string
|
||||
type: object
|
||||
dto.SuperOrderItem:
|
||||
properties:
|
||||
amount_discount:
|
||||
@@ -1958,9 +1967,24 @@ definitions:
|
||||
allOf:
|
||||
- $ref: '#/definitions/consts.Currency'
|
||||
description: Currency 币种。
|
||||
flag_reason:
|
||||
description: FlagReason 问题标记原因。
|
||||
type: string
|
||||
flagged_at:
|
||||
description: FlaggedAt 标记时间(RFC3339)。
|
||||
type: string
|
||||
flagged_by:
|
||||
description: FlaggedBy 标记操作者ID。
|
||||
type: integer
|
||||
id:
|
||||
description: ID 订单ID。
|
||||
type: integer
|
||||
is_flagged:
|
||||
description: IsFlagged 是否标记为问题订单。
|
||||
type: boolean
|
||||
is_reconciled:
|
||||
description: IsReconciled 是否完成对账。
|
||||
type: boolean
|
||||
items:
|
||||
description: Items 订单明细行,用于展示具体内容与金额拆分。
|
||||
items:
|
||||
@@ -1969,6 +1993,15 @@ definitions:
|
||||
paid_at:
|
||||
description: PaidAt 支付时间(RFC3339)。
|
||||
type: string
|
||||
reconcile_note:
|
||||
description: ReconcileNote 对账说明。
|
||||
type: string
|
||||
reconciled_at:
|
||||
description: ReconciledAt 对账时间(RFC3339)。
|
||||
type: string
|
||||
reconciled_by:
|
||||
description: ReconciledBy 对账操作者ID。
|
||||
type: integer
|
||||
refunded_at:
|
||||
description: RefundedAt 退款时间(RFC3339)。
|
||||
type: string
|
||||
@@ -2007,6 +2040,15 @@ definitions:
|
||||
snapshot:
|
||||
description: Snapshot 明细快照,用于展示内容标题等历史信息。
|
||||
type: object
|
||||
dto.SuperOrderReconcileForm:
|
||||
properties:
|
||||
is_reconciled:
|
||||
description: IsReconciled 是否完成对账。
|
||||
type: boolean
|
||||
note:
|
||||
description: Note 对账说明(可选)。
|
||||
type: string
|
||||
type: object
|
||||
dto.SuperOrderRefundForm:
|
||||
properties:
|
||||
force:
|
||||
@@ -4028,6 +4070,62 @@ paths:
|
||||
summary: Get order
|
||||
tags:
|
||||
- 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:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
|
||||
### 2.8 订单与退款 `/superadmin/orders`
|
||||
- 状态:**已完成**
|
||||
- 已有:列表、详情、退款申请。
|
||||
- 缺口:问题订单标记/支付对账等运营增强能力。
|
||||
- 已有:列表、详情、退款申请、问题订单标记、对账辅助。
|
||||
- 缺口:无显著功能缺口。
|
||||
|
||||
### 2.9 创作者与成员审核 `/superadmin/creators`
|
||||
- 状态:**部分完成**
|
||||
@@ -92,6 +92,5 @@
|
||||
|
||||
## 4) 建议的下一步(按优先级)
|
||||
|
||||
1. **订单运营补强**:问题订单标记、支付对账辅助能力。
|
||||
2. **内容批量处置扩展**:补齐内容/举报的批量下架、封禁与归档处理。
|
||||
3. **创作者治理补强**:结算账户审批流、提现审核联动流程。
|
||||
1. **内容批量处置扩展**:补齐内容/举报的批量下架、封禁与归档处理。
|
||||
2. **创作者治理补强**:结算账户审批流、提现审核联动流程。
|
||||
|
||||
@@ -20,6 +20,8 @@ export const OrderService = {
|
||||
content_title,
|
||||
type,
|
||||
status,
|
||||
is_flagged,
|
||||
is_reconciled,
|
||||
created_at_from,
|
||||
created_at_to,
|
||||
paid_at_from,
|
||||
@@ -49,6 +51,8 @@ export const OrderService = {
|
||||
content_title,
|
||||
type,
|
||||
status,
|
||||
is_flagged,
|
||||
is_reconciled,
|
||||
created_at_from: iso(created_at_from),
|
||||
created_at_to: iso(created_at_to),
|
||||
paid_at_from: iso(paid_at_from),
|
||||
@@ -86,5 +90,25 @@ export const OrderService = {
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,6 +17,16 @@ const refundLoading = ref(false);
|
||||
const refundForce = ref(false);
|
||||
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) {
|
||||
if (!value) 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) {
|
||||
try {
|
||||
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(
|
||||
() => orderID.value,
|
||||
() => loadDetail(),
|
||||
@@ -104,6 +182,8 @@ watch(
|
||||
<span class="text-muted-color">OrderID: {{ orderID || '-' }}</span>
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,6 +220,26 @@ watch(
|
||||
<div class="text-sm text-muted-color">支付时间</div>
|
||||
<div class="font-medium">{{ formatDate(detail?.order?.paid_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_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 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'" />
|
||||
</template>
|
||||
</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>
|
||||
|
||||
@@ -32,6 +32,8 @@ const contentID = ref(null);
|
||||
const contentTitle = ref('');
|
||||
const status = ref('');
|
||||
const type = ref('');
|
||||
const isFlagged = ref(null);
|
||||
const isReconciled = ref(null);
|
||||
const createdAtFrom = ref(null);
|
||||
const createdAtTo = ref(null);
|
||||
const paidAtFrom = ref(null);
|
||||
@@ -56,6 +58,12 @@ const typeOptions = [
|
||||
{ label: 'content_purchase', value: 'content_purchase' }
|
||||
];
|
||||
|
||||
const booleanOptions = [
|
||||
{ label: '全部', value: null },
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false }
|
||||
];
|
||||
|
||||
function getQueryValue(value) {
|
||||
if (Array.isArray(value)) return value[0];
|
||||
return value ?? null;
|
||||
@@ -74,6 +82,15 @@ function parseDate(value) {
|
||||
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) {
|
||||
if (!value) return '-';
|
||||
if (String(value).startsWith('0001-01-01')) return '-';
|
||||
@@ -93,6 +110,8 @@ function resetFilters() {
|
||||
contentTitle.value = '';
|
||||
status.value = '';
|
||||
type.value = '';
|
||||
isFlagged.value = null;
|
||||
isReconciled.value = null;
|
||||
createdAtFrom.value = null;
|
||||
createdAtTo.value = null;
|
||||
paidAtFrom.value = null;
|
||||
@@ -121,6 +140,11 @@ function applyRouteQuery(query) {
|
||||
if (statusValue !== null) status.value = String(statusValue);
|
||||
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 createdToValue = getQueryValue(query?.created_at_to);
|
||||
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() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -169,6 +201,8 @@ async function loadOrders() {
|
||||
content_title: contentTitle.value,
|
||||
status: status.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_to: createdAtTo.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) {
|
||||
refundOrder.value = order;
|
||||
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() {
|
||||
page.value = 1;
|
||||
loadOrders();
|
||||
@@ -289,6 +393,12 @@ watch(
|
||||
<SearchField label="类型">
|
||||
<Select v-model="type" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||
</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">
|
||||
<DatePicker v-model="createdAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||
</SearchField>
|
||||
@@ -358,6 +468,24 @@ watch(
|
||||
<Tag :value="data?.status_description || data?.status || '-'" :severity="getOrderStatusSeverity(data?.status)" />
|
||||
</template>
|
||||
</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">
|
||||
<template #body="{ data }">
|
||||
{{ formatCny(data.amount_paid) }}
|
||||
@@ -390,7 +518,11 @@ watch(
|
||||
</Column>
|
||||
<Column header="操作" style="min-width: 10rem">
|
||||
<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>
|
||||
</Column>
|
||||
</DataTable>
|
||||
@@ -421,4 +553,56 @@ watch(
|
||||
<Button label="确认退款" icon="pi pi-check" severity="danger" @click="confirmRefund" :loading="refundLoading" :disabled="refundOrder?.status !== 'paid'" />
|
||||
</template>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user