增加流水通知审计分页
This commit is contained in:
@@ -7,10 +7,18 @@ import (
|
||||
)
|
||||
|
||||
type Query struct {
|
||||
ActorID uint64
|
||||
Action string
|
||||
BizType string
|
||||
Limit int
|
||||
ActorID uint64
|
||||
Action string
|
||||
BizType string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type PaginatedResult struct {
|
||||
Items interface{} `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type LogDTO struct {
|
||||
|
||||
@@ -18,17 +18,32 @@ func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
func parsePagination(c *gin.Context) (int, int) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
query, ok := parseQuery(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.List(query)
|
||||
result, err := h.service.List(query)
|
||||
if err != nil {
|
||||
writeAuditError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func parseQuery(c *gin.Context) (Query, bool) {
|
||||
@@ -41,16 +56,11 @@ func parseQuery(c *gin.Context) (Query, bool) {
|
||||
}
|
||||
query.ActorID = value
|
||||
}
|
||||
if raw := c.Query("limit"); raw != "" {
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value <= 0 {
|
||||
response.BadRequest(c, "查询条数不正确")
|
||||
return query, false
|
||||
}
|
||||
query.Limit = value
|
||||
}
|
||||
query.Action = c.Query("action")
|
||||
query.BizType = c.Query("biz_type")
|
||||
page, pageSize := parsePagination(c)
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
return query, true
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package adminaudit
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -15,37 +17,42 @@ func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
func (r *Repository) List(query Query) ([]LogDTO, error) {
|
||||
limit := query.Limit
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 200
|
||||
}
|
||||
|
||||
func (r *Repository) List(query Query) (*PaginatedResult, error) {
|
||||
db := r.db.Table("audit_logs AS al").
|
||||
Select(`al.id, al.actor_type, al.actor_id, COALESCE(au.username, '') AS actor_username,
|
||||
COALESCE(au.nickname, '') AS actor_nickname, al.action, al.biz_type, al.biz_id,
|
||||
al.ip, al.user_agent, al.detail, al.created_at`).
|
||||
Joins("LEFT JOIN admin_users AS au ON au.id = al.actor_id AND al.actor_type = ?", "admin")
|
||||
|
||||
countDB := r.db.Model(&model.AuditLog{})
|
||||
if query.ActorID > 0 {
|
||||
db = db.Where("al.actor_id = ?", query.ActorID)
|
||||
countDB = countDB.Where("actor_id = ?", query.ActorID)
|
||||
}
|
||||
if query.Action != "" {
|
||||
db = db.Where("al.action = ?", query.Action)
|
||||
countDB = countDB.Where("action = ?", query.Action)
|
||||
}
|
||||
if query.BizType != "" {
|
||||
db = db.Where("al.biz_type = ?", query.BizType)
|
||||
countDB = countDB.Where("biz_type = ?", query.BizType)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offset := (query.Page - 1) * query.PageSize
|
||||
var rows []auditLogRow
|
||||
if err := db.Order("al.id DESC").Limit(limit).Scan(&rows).Error; err != nil {
|
||||
if err := db.Order("al.id DESC").Offset(offset).Limit(query.PageSize).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]LogDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, row.toDTO())
|
||||
}
|
||||
return items, nil
|
||||
return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
|
||||
}
|
||||
|
||||
type auditLogRow struct {
|
||||
|
||||
@@ -12,7 +12,7 @@ func NewService(repo *Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) List(query Query) ([]LogDTO, error) {
|
||||
func (s *Service) List(query Query) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
|
||||
@@ -13,3 +13,10 @@ type NotificationDTO struct {
|
||||
ReadAt *time.Time `json:"read_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type PaginatedResult struct {
|
||||
Items interface{} `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
@@ -18,18 +18,34 @@ func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
func parsePagination(c *gin.Context) (int, int) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
items, err := h.service.List(userID)
|
||||
page, pageSize := parsePagination(c)
|
||||
result, err := h.service.List(userID, page, pageSize)
|
||||
if err != nil {
|
||||
writeNotificationError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *Handler) MarkRead(c *gin.Context) {
|
||||
|
||||
@@ -25,16 +25,21 @@ func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
func (r *Repository) List(userID uint64) ([]NotificationDTO, error) {
|
||||
func (r *Repository) List(userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
var total int64
|
||||
if err := r.db.Model(&model.Notification{}).Where("user_id = ?", userID).Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
var rows []model.Notification
|
||||
if err := r.db.Where("user_id = ?", userID).Order("id DESC").Limit(100).Find(&rows).Error; err != nil {
|
||||
if err := r.db.Where("user_id = ?", userID).Order("id DESC").Offset(offset).Limit(pageSize).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]NotificationDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, toDTO(row))
|
||||
}
|
||||
return items, nil
|
||||
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) MarkRead(userID uint64, id uint64) error {
|
||||
|
||||
@@ -12,11 +12,11 @@ func NewService(repo *Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) List(userID uint64) ([]NotificationDTO, error) {
|
||||
func (s *Service) List(userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.List(userID)
|
||||
return s.repo.List(userID, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *Service) MarkRead(userID uint64, id uint64) error {
|
||||
|
||||
@@ -29,10 +29,18 @@ type LedgerDTO struct {
|
||||
}
|
||||
|
||||
type AdminLedgerQuery struct {
|
||||
UserID uint64
|
||||
OrderID uint64
|
||||
BizType string
|
||||
Limit int
|
||||
UserID uint64
|
||||
OrderID uint64
|
||||
BizType string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type PaginatedResult struct {
|
||||
Items interface{} `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type AdminLedgerDTO struct {
|
||||
|
||||
@@ -32,18 +32,34 @@ func (h *Handler) Balance(c *gin.Context) {
|
||||
response.OK(c, account)
|
||||
}
|
||||
|
||||
func parsePagination(c *gin.Context) (int, int) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func (h *Handler) Ledger(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
items, err := h.service.Ledger(userID)
|
||||
page, pageSize := parsePagination(c)
|
||||
result, err := h.service.Ledger(userID, page, pageSize)
|
||||
if err != nil {
|
||||
writeWalletError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *Handler) Recharge(c *gin.Context) {
|
||||
@@ -70,12 +86,12 @@ func (h *Handler) AdminLedger(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.AdminLedger(query)
|
||||
result, err := h.service.AdminLedger(query)
|
||||
if err != nil {
|
||||
writeWalletError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func parseAdminLedgerQuery(c *gin.Context) (AdminLedgerQuery, bool) {
|
||||
@@ -96,15 +112,10 @@ func parseAdminLedgerQuery(c *gin.Context) (AdminLedgerQuery, bool) {
|
||||
}
|
||||
query.OrderID = value
|
||||
}
|
||||
if raw := c.Query("limit"); raw != "" {
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value <= 0 {
|
||||
response.BadRequest(c, "查询条数不正确")
|
||||
return query, false
|
||||
}
|
||||
query.Limit = value
|
||||
}
|
||||
query.BizType = c.Query("biz_type")
|
||||
page, pageSize := parsePagination(c)
|
||||
query.Page = page
|
||||
query.PageSize = pageSize
|
||||
return query, true
|
||||
}
|
||||
|
||||
|
||||
@@ -45,16 +45,21 @@ func (r *Repository) Account(userID uint64) (*AccountDTO, error) {
|
||||
return toAccountDTO(account), nil
|
||||
}
|
||||
|
||||
func (r *Repository) Ledger(userID uint64) ([]LedgerDTO, error) {
|
||||
func (r *Repository) Ledger(userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
var total int64
|
||||
if err := r.db.Model(&model.WalletLedger{}).Where("user_id = ?", userID).Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
var rows []model.WalletLedger
|
||||
if err := r.db.Where("user_id = ?", userID).Order("id DESC").Limit(100).Find(&rows).Error; err != nil {
|
||||
if err := r.db.Where("user_id = ?", userID).Order("id DESC").Offset(offset).Limit(pageSize).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]LedgerDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, toLedgerDTO(row))
|
||||
}
|
||||
return items, nil
|
||||
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Recharge(userID uint64, amount float64) (*AccountDTO, error) {
|
||||
@@ -75,12 +80,7 @@ func (r *Repository) Recharge(userID uint64, amount float64) (*AccountDTO, error
|
||||
return r.Account(userID)
|
||||
}
|
||||
|
||||
func (r *Repository) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
|
||||
limit := query.Limit
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 200
|
||||
}
|
||||
|
||||
func (r *Repository) AdminLedger(query AdminLedgerQuery) (*PaginatedResult, error) {
|
||||
db := r.db.Table("wallet_ledger AS wl").
|
||||
Select(`wl.id, wl.ledger_no, wl.user_id, COALESCE(u.phone, '') AS user_phone,
|
||||
COALESCE(u.nickname, '') AS user_nickname, wl.order_id, COALESCE(ro.order_no, '') AS order_no,
|
||||
@@ -89,21 +89,31 @@ func (r *Repository) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, erro
|
||||
Joins("LEFT JOIN users AS u ON u.id = wl.user_id").
|
||||
Joins("LEFT JOIN rental_orders AS ro ON ro.id = wl.order_id")
|
||||
|
||||
countDB := r.db.Model(&model.WalletLedger{})
|
||||
if query.UserID > 0 {
|
||||
db = db.Where("wl.user_id = ?", query.UserID)
|
||||
countDB = countDB.Where("user_id = ?", query.UserID)
|
||||
}
|
||||
if query.OrderID > 0 {
|
||||
db = db.Where("wl.order_id = ?", query.OrderID)
|
||||
countDB = countDB.Where("order_id = ?", query.OrderID)
|
||||
}
|
||||
if query.BizType != "" {
|
||||
db = db.Where("wl.biz_type = ?", query.BizType)
|
||||
countDB = countDB.Where("biz_type = ?", query.BizType)
|
||||
}
|
||||
|
||||
var items []AdminLedgerDTO
|
||||
if err := db.Order("wl.id DESC").Limit(limit).Scan(&items).Error; err != nil {
|
||||
var total int64
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
|
||||
offset := (query.Page - 1) * query.PageSize
|
||||
var items []AdminLedgerDTO
|
||||
if err := db.Order("wl.id DESC").Offset(offset).Limit(query.PageSize).Scan(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
|
||||
}
|
||||
|
||||
func AppendEntries(tx *gorm.DB, entries ...Entry) error {
|
||||
|
||||
@@ -23,11 +23,11 @@ func (s *Service) Account(userID uint64) (*AccountDTO, error) {
|
||||
return s.repo.Account(userID)
|
||||
}
|
||||
|
||||
func (s *Service) Ledger(userID uint64) ([]LedgerDTO, error) {
|
||||
func (s *Service) Ledger(userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.Ledger(userID)
|
||||
return s.repo.Ledger(userID, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *Service) Recharge(userID uint64, req RechargeRequest) (*AccountDTO, error) {
|
||||
@@ -40,7 +40,7 @@ func (s *Service) Recharge(userID uint64, req RechargeRequest) (*AccountDTO, err
|
||||
return s.repo.Recharge(userID, req.Amount)
|
||||
}
|
||||
|
||||
func (s *Service) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
|
||||
func (s *Service) AdminLedger(query AdminLedgerQuery) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
|
||||
export interface AdminAuditLog {
|
||||
id: number
|
||||
actor_type: string
|
||||
@@ -19,7 +21,8 @@ export interface AdminAuditQuery {
|
||||
actor_id?: string
|
||||
action?: string
|
||||
biz_type?: string
|
||||
limit?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
@@ -30,6 +33,6 @@ interface ApiResponse<T> {
|
||||
|
||||
export async function fetchAdminAuditLogs(query: AdminAuditQuery = {}) {
|
||||
const params = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== '' && value !== undefined))
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: AdminAuditLog[] }>>('/admin/audit-logs', { params })
|
||||
return data.data.items
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminAuditLog>>>('/admin/audit-logs', { params })
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
|
||||
export interface AdminWalletLedger {
|
||||
id: number
|
||||
ledger_no: string
|
||||
@@ -22,7 +24,8 @@ export interface AdminWalletLedgerQuery {
|
||||
user_id?: string
|
||||
order_id?: string
|
||||
biz_type?: string
|
||||
limit?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
@@ -33,6 +36,6 @@ interface ApiResponse<T> {
|
||||
|
||||
export async function fetchAdminWalletLedger(query: AdminWalletLedgerQuery = {}) {
|
||||
const params = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== '' && value !== undefined))
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: AdminWalletLedger[] }>>('/admin/wallet/ledger', { params })
|
||||
return data.data.items
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminWalletLedger>>>('/admin/wallet/ledger', { params })
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
|
||||
export interface NotificationItem {
|
||||
id: number
|
||||
user_id: number
|
||||
@@ -18,9 +20,11 @@ interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchNotifications() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: NotificationItem[] }>>('/notifications')
|
||||
return data.data.items
|
||||
export async function fetchNotifications(page = 1, pageSize = 20) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<NotificationItem>>>('/notifications', {
|
||||
params: { page, page_size: pageSize },
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function markNotificationRead(id: number) {
|
||||
|
||||
@@ -84,6 +84,13 @@ interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export async function createOrder(listingId: number) {
|
||||
const { data } = await apiClient.post<ApiResponse<Order>>('/orders', {
|
||||
listing_id: listingId,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
|
||||
export interface WalletAccount {
|
||||
user_id: number
|
||||
available_balance: number
|
||||
@@ -33,9 +35,11 @@ export async function fetchWalletBalance() {
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchWalletLedger() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: WalletLedger[] }>>('/wallet/ledger')
|
||||
return data.data.items
|
||||
export async function fetchWalletLedger(page = 1, pageSize = 20) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<WalletLedger>>>('/wallet/ledger', {
|
||||
params: { page, page_size: pageSize },
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function rechargeWallet(amount: number) {
|
||||
|
||||
@@ -7,18 +7,28 @@ import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
const notifications = ref<NotificationItem[]>([])
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
onMounted(loadNotifications)
|
||||
|
||||
async function loadNotifications() {
|
||||
loading.value = true
|
||||
try {
|
||||
notifications.value = await fetchNotifications()
|
||||
const result = await fetchNotifications(currentPage.value, currentPageSize.value)
|
||||
notifications.value = result.items
|
||||
total.value = result.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
currentPage.value = 1
|
||||
loadNotifications()
|
||||
}
|
||||
|
||||
async function markRead(id: number) {
|
||||
await markNotificationRead(id)
|
||||
ElMessage.success('已标记为已读')
|
||||
@@ -51,5 +61,17 @@ async function markRead(id: number) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pagination-wrap" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="loadNotifications"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -9,6 +9,9 @@ import { formatDateTime } from '@/utils/time'
|
||||
const loading = ref(false)
|
||||
const account = ref<WalletAccount | null>(null)
|
||||
const ledger = ref<WalletLedger[]>([])
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const rechargeAmount = ref(100)
|
||||
const recharging = ref(false)
|
||||
|
||||
@@ -17,14 +20,24 @@ onMounted(loadWallet)
|
||||
async function loadWallet() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [balance, rows] = await Promise.all([fetchWalletBalance(), fetchWalletLedger()])
|
||||
const [balance, result] = await Promise.all([fetchWalletBalance(), fetchWalletLedger(currentPage.value, currentPageSize.value)])
|
||||
account.value = balance
|
||||
ledger.value = rows
|
||||
ledger.value = result.items
|
||||
total.value = result.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
currentPage.value = 1
|
||||
loadWallet()
|
||||
}
|
||||
|
||||
function loadLedgerPage() {
|
||||
loadWallet()
|
||||
}
|
||||
|
||||
async function handleRecharge() {
|
||||
recharging.value = true
|
||||
try {
|
||||
@@ -92,6 +105,18 @@ function readError(error: unknown, fallback: string) {
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrap" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="loadLedgerPage"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -8,22 +8,30 @@ import { formatDateTime } from '@/utils/time'
|
||||
const loading = ref(false)
|
||||
const logs = ref<AdminAuditLog[]>([])
|
||||
const activeLog = ref<AdminAuditLog | null>(null)
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const filters = reactive({
|
||||
actor_id: '',
|
||||
action: '',
|
||||
biz_type: '',
|
||||
limit: 200,
|
||||
})
|
||||
|
||||
const highRiskCount = computed(() => logs.value.filter((item) => item.action.includes('freeze') || item.action.includes('update')).length)
|
||||
const uniqueActors = computed(() => new Set(logs.value.map((item) => item.actor_id)).size)
|
||||
|
||||
onMounted(loadLogs)
|
||||
|
||||
async function loadLogs() {
|
||||
loading.value = true
|
||||
try {
|
||||
logs.value = await fetchAdminAuditLogs(filters)
|
||||
const query = {
|
||||
...filters,
|
||||
page: currentPage.value,
|
||||
page_size: currentPageSize.value,
|
||||
}
|
||||
const result = await fetchAdminAuditLogs(query)
|
||||
logs.value = result.items
|
||||
total.value = result.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -33,10 +41,15 @@ function resetFilters() {
|
||||
filters.actor_id = ''
|
||||
filters.action = ''
|
||||
filters.biz_type = ''
|
||||
filters.limit = 200
|
||||
currentPage.value = 1
|
||||
void loadLogs()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
currentPage.value = 1
|
||||
loadLogs()
|
||||
}
|
||||
|
||||
function actorName(row: AdminAuditLog) {
|
||||
return row.actor_nickname || row.actor_username || `${row.actor_type} ${row.actor_id}`
|
||||
}
|
||||
@@ -59,7 +72,7 @@ function actionType(action: string) {
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Audit Logs</p>
|
||||
<h1>审计日志</h1>
|
||||
<p>查看后台管理员操作、业务对象、来源 IP 和操作明细,用于追踪冻结、配置修改等高风险动作。</p>
|
||||
<p>查看后台管理员操作、业务对象和操作明细,用于追踪冻结、配置修改等高风险动作。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
@@ -69,12 +82,8 @@ function actionType(action: string) {
|
||||
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card">
|
||||
<span>当前结果</span>
|
||||
<strong>{{ logs.length }} 条</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>操作管理员</span>
|
||||
<strong>{{ uniqueActors }} 人</strong>
|
||||
<span>总条数</span>
|
||||
<strong>{{ total }} 条</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>高风险动作</span>
|
||||
@@ -94,13 +103,8 @@ function actionType(action: string) {
|
||||
<el-option label="商品标记异常" value="listing.mark_abnormal" />
|
||||
<el-option label="客服关闭订单" value="order.admin_close" />
|
||||
<el-option label="订单标记异常" value="order.mark_abnormal" />
|
||||
<el-option label="号主交接超时" value="order.timeout.owner_submit" />
|
||||
<el-option label="租客确认超时" value="order.timeout.renter_confirm" />
|
||||
<el-option label="租客归还逾期" value="order.timeout.return_overdue" />
|
||||
<el-option label="号主确认归还超时" value="order.timeout.owner_return_confirm" />
|
||||
<el-option label="申诉仲裁" value="dispute.arbitrate" />
|
||||
<el-option label="更新系统配置" value="system_config.update" />
|
||||
<el-option label="创建系统配置" value="system_config.create" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务类型">
|
||||
@@ -112,17 +116,12 @@ function actionType(action: string) {
|
||||
<el-option label="系统配置" value="system_config" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="查询条数">
|
||||
<el-input-number v-model="filters.limit" :min="20" :max="500" :step="20" class="full-control" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="logs">
|
||||
<el-table-column prop="id" label="ID" width="90" />
|
||||
<el-table-column label="管理员" min-width="160">
|
||||
<el-table-column label="操作人" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<strong>{{ actorName(row) }}</strong>
|
||||
<span class="table-subtext">{{ row.actor_type }} ID: {{ row.actor_id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="动作" min-width="180">
|
||||
@@ -132,8 +131,6 @@ function actionType(action: string) {
|
||||
</el-table-column>
|
||||
<el-table-column prop="biz_type" label="业务类型" width="130" />
|
||||
<el-table-column prop="biz_id" label="业务 ID" width="100" />
|
||||
<el-table-column prop="ip" label="IP" min-width="130" />
|
||||
<el-table-column prop="user_agent" label="User-Agent" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
@@ -144,9 +141,23 @@ function actionType(action: string) {
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrap" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="loadLogs"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog :model-value="!!activeLog" title="审计明细" width="720px" @update:model-value="activeLog = null">
|
||||
<div v-if="activeLog" class="dialog-body">
|
||||
<p><strong>{{ activeLog.action }}</strong> · {{ activeLog.biz_type }} #{{ activeLog.biz_id || '-' }}</p>
|
||||
<p>操作人:{{ actorName(activeLog) }} · IP:{{ activeLog.ip }}</p>
|
||||
<p>User-Agent:{{ activeLog.user_agent }}</p>
|
||||
<div class="code-panel">
|
||||
<pre>{{ detailText(activeLog) }}</pre>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,13 @@ import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
const ledger = ref<AdminWalletLedger[]>([])
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const filters = reactive({
|
||||
user_id: '',
|
||||
order_id: '',
|
||||
biz_type: '',
|
||||
limit: 200,
|
||||
})
|
||||
|
||||
const inAmount = computed(() =>
|
||||
@@ -21,16 +23,20 @@ const inAmount = computed(() =>
|
||||
const outAmount = computed(() =>
|
||||
ledger.value.filter((item) => item.direction === 'out').reduce((sum, item) => sum + Number(item.amount || 0), 0),
|
||||
)
|
||||
const frozenAmount = computed(() =>
|
||||
ledger.value.filter((item) => item.balance_type === 'frozen').reduce((sum, item) => sum + Number(item.amount || 0), 0),
|
||||
)
|
||||
|
||||
onMounted(loadLedger)
|
||||
|
||||
async function loadLedger() {
|
||||
loading.value = true
|
||||
try {
|
||||
ledger.value = await fetchAdminWalletLedger(filters)
|
||||
const query = {
|
||||
...filters,
|
||||
page: currentPage.value,
|
||||
page_size: currentPageSize.value,
|
||||
}
|
||||
const result = await fetchAdminWalletLedger(query)
|
||||
ledger.value = result.items
|
||||
total.value = result.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -40,10 +46,15 @@ function resetFilters() {
|
||||
filters.user_id = ''
|
||||
filters.order_id = ''
|
||||
filters.biz_type = ''
|
||||
filters.limit = 200
|
||||
currentPage.value = 1
|
||||
void loadLedger()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
currentPage.value = 1
|
||||
loadLedger()
|
||||
}
|
||||
|
||||
function money(value: number) {
|
||||
return `¥${value.toFixed(2)}`
|
||||
}
|
||||
@@ -65,7 +76,7 @@ function directionLabel(direction: string) {
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Wallet Ledger</p>
|
||||
<h1>资金流水</h1>
|
||||
<p>查看订单金额、押金、冻结、解冻、退款和结算流水。当前为开发态账务记录,不代表真实支付余额。</p>
|
||||
<p>查看订单金额、押金、冻结、解冻、退款和结算流水。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
@@ -75,8 +86,8 @@ function directionLabel(direction: string) {
|
||||
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card">
|
||||
<span>当前结果</span>
|
||||
<strong>{{ ledger.length }} 笔</strong>
|
||||
<span>总条数</span>
|
||||
<strong>{{ total }} 笔</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>入账合计</span>
|
||||
@@ -86,10 +97,6 @@ function directionLabel(direction: string) {
|
||||
<span>出账合计</span>
|
||||
<strong>{{ money(outAmount) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>冻结相关金额</span>
|
||||
<strong>{{ money(frozenAmount) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form class="filter-panel" label-position="top">
|
||||
@@ -107,20 +114,16 @@ function directionLabel(direction: string) {
|
||||
<el-option label="押金退回" value="deposit_refund" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="查询条数">
|
||||
<el-input-number v-model="filters.limit" :min="20" :max="500" :step="20" class="full-control" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="ledger">
|
||||
<el-table-column prop="ledger_no" label="流水号" min-width="230" />
|
||||
<el-table-column label="用户" min-width="160">
|
||||
<el-table-column label="用户" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<strong>{{ row.user_phone || `用户 ${row.user_id}` }}</strong>
|
||||
<span class="table-subtext">ID: {{ row.user_id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单" min-width="180">
|
||||
<el-table-column label="订单" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<RouterLink v-if="row.order_id" :to="`/admin/orders/${row.order_id}`">{{ row.order_no || row.order_id }}</RouterLink>
|
||||
<span v-else>-</span>
|
||||
@@ -141,10 +144,21 @@ function directionLabel(direction: string) {
|
||||
<el-table-column label="变化后余额" width="130">
|
||||
<template #default="{ row }">{{ money(Number(row.balance_after)) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="220" />
|
||||
<el-table-column label="创建时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrap" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="loadLedger"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user