feat: add order governance flags and reconciliation
This commit is contained in:
@@ -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 内容摘要信息。
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"`
|
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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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. **创作者治理补强**:结算账户审批流、提现审核联动流程。
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user