后台“资金流水

This commit is contained in:
yml
2026-05-22 17:43:45 +08:00
parent 7df557d668
commit c9e6ea2b71
28 changed files with 1271 additions and 34 deletions
+23
View File
@@ -0,0 +1,23 @@
package adminuser
import "time"
type UserDTO struct {
ID uint64 `json:"id"`
Phone string `json:"phone"`
Nickname string `json:"nickname"`
RealnameStatus string `json:"realname_status"`
RiskStatus string `json:"risk_status"`
CreditScore int `json:"credit_score"`
Status string `json:"status"`
OrderCount int64 `json:"order_count"`
ListingCount int64 `json:"listing_count"`
DisputeCount int64 `json:"dispute_count"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type FreezeRequest struct {
Reason string `json:"reason"`
}
@@ -0,0 +1,105 @@
package adminuser
import (
"errors"
"net/http"
"strconv"
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/pkg/response"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) List(c *gin.Context) {
items, err := h.service.List()
if err != nil {
writeAdminUserError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func (h *Handler) Freeze(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
userID, ok := parseID(c)
if !ok {
return
}
var req FreezeRequest
_ = c.ShouldBindJSON(&req)
item, err := h.service.Freeze(adminID, userID, req, auditMeta(c))
if err != nil {
writeAdminUserError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) Unfreeze(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
userID, ok := parseID(c)
if !ok {
return
}
item, err := h.service.Unfreeze(adminID, userID, auditMeta(c))
if err != nil {
writeAdminUserError(c, err)
return
}
response.OK(c, item)
}
func currentAdminID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextAdminID)
if !ok {
return 0, false
}
adminID, ok := value.(uint64)
return adminID, ok
}
func parseID(c *gin.Context) (uint64, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
response.BadRequest(c, "ID 不正确")
return 0, false
}
return id, true
}
func auditMeta(c *gin.Context) AuditMeta {
return AuditMeta{
IP: c.ClientIP(),
UserAgent: c.GetHeader("User-Agent"),
}
}
func writeAdminUserError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "数据库未连接")
case errors.Is(err, ErrInvalidUser):
response.BadRequest(c, "用户不符合规则")
case IsNotFound(err):
response.Error(c, http.StatusNotFound, "not_found", "用户不存在")
default:
response.Error(c, http.StatusInternalServerError, "internal_error", "用户管理服务暂时不可用")
}
}
@@ -0,0 +1,150 @@
package adminuser
import (
"encoding/json"
"errors"
"hfb_sys/backend/internal/model"
"gorm.io/datatypes"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Repository struct {
db *gorm.DB
}
type AuditMeta struct {
IP string
UserAgent string
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) List() ([]UserDTO, error) {
var rows []userRow
err := r.db.Table("users AS u").
Select(`u.*,
COALESCE(o.order_count, 0) AS order_count,
COALESCE(l.listing_count, 0) AS listing_count,
COALESCE(d.dispute_count, 0) AS dispute_count`).
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id").
Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id").
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id").
Order("u.id DESC").
Limit(200).
Scan(&rows).Error
if err != nil {
return nil, err
}
items := make([]UserDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toDTO())
}
return items, nil
}
func (r *Repository) Freeze(adminID uint64, userID uint64, req FreezeRequest, meta AuditMeta) (*UserDTO, error) {
return r.updateStatus(adminID, userID, "frozen", "frozen", "admin_user.freeze", req.Reason, meta)
}
func (r *Repository) Unfreeze(adminID uint64, userID uint64, meta AuditMeta) (*UserDTO, error) {
return r.updateStatus(adminID, userID, "active", "normal", "admin_user.unfreeze", "", meta)
}
func (r *Repository) updateStatus(adminID uint64, userID uint64, status string, riskStatus string, action string, reason string, meta AuditMeta) (*UserDTO, error) {
err := r.db.Transaction(func(tx *gorm.DB) error {
var user model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
return err
}
beforeStatus := user.Status
beforeRisk := user.RiskStatus
user.Status = status
user.RiskStatus = riskStatus
if err := tx.Save(&user).Error; err != nil {
return err
}
return appendAuditLog(tx, adminID, action, user.ID, meta, map[string]any{
"user_id": user.ID,
"reason": reason,
"before_status": beforeStatus,
"after_status": status,
"before_risk_status": beforeRisk,
"after_risk_status": riskStatus,
})
})
if err != nil {
return nil, err
}
return r.Find(userID)
}
func (r *Repository) Find(userID uint64) (*UserDTO, error) {
var row userRow
err := r.db.Table("users AS u").
Select(`u.*,
COALESCE(o.order_count, 0) AS order_count,
COALESCE(l.listing_count, 0) AS listing_count,
COALESCE(d.dispute_count, 0) AS dispute_count`).
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id").
Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id").
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id").
Where("u.id = ?", userID).
First(&row).Error
if err != nil {
return nil, err
}
dto := row.toDTO()
return &dto, nil
}
type userRow struct {
model.User
OrderCount int64
ListingCount int64
DisputeCount int64
}
func (row userRow) toDTO() UserDTO {
return UserDTO{
ID: row.ID,
Phone: row.Phone,
Nickname: row.Nickname,
RealnameStatus: row.RealnameStatus,
RiskStatus: row.RiskStatus,
CreditScore: row.CreditScore,
Status: row.Status,
OrderCount: row.OrderCount,
ListingCount: row.ListingCount,
DisputeCount: row.DisputeCount,
LastLoginAt: row.LastLoginAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error {
raw, err := json.Marshal(detail)
if err != nil {
return err
}
row := model.AuditLog{
ActorType: "admin",
ActorID: actorID,
Action: action,
BizType: "user",
BizID: &bizID,
IP: meta.IP,
UserAgent: meta.UserAgent,
Detail: datatypes.JSON(raw),
}
return tx.Create(&row).Error
}
func IsNotFound(err error) bool {
return errors.Is(err, gorm.ErrRecordNotFound)
}
@@ -0,0 +1,43 @@
package adminuser
import "errors"
var (
ErrDependencyUnavailable = errors.New("dependency unavailable")
ErrInvalidUser = errors.New("invalid user")
)
type Service struct {
repo *Repository
}
func NewService(repo *Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) List() ([]UserDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.List()
}
func (s *Service) Freeze(adminID uint64, userID uint64, req FreezeRequest, meta AuditMeta) (*UserDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if userID == 0 {
return nil, ErrInvalidUser
}
return s.repo.Freeze(adminID, userID, req, meta)
}
func (s *Service) Unfreeze(adminID uint64, userID uint64, meta AuditMeta) (*UserDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if userID == 0 {
return nil, ErrInvalidUser
}
return s.repo.Unfreeze(adminID, userID, meta)
}
+2
View File
@@ -13,6 +13,8 @@ type OrderDTO struct {
AccountID uint64 `json:"account_id"`
OwnerID uint64 `json:"owner_id"`
RenterID uint64 `json:"renter_id"`
OwnerPhone string `json:"owner_phone,omitempty"`
RenterPhone string `json:"renter_phone,omitempty"`
Title string `json:"title"`
ServerRegion string `json:"server_region"`
LoginPlatform string `json:"login_platform"`
+35
View File
@@ -52,6 +52,41 @@ func (h *Handler) List(c *gin.Context) {
response.OK(c, gin.H{"items": items})
}
func (h *Handler) AdminList(c *gin.Context) {
items, err := h.service.ListAdmin()
if err != nil {
writeOrderError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func (h *Handler) AdminDetail(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
item, err := h.service.FindAdmin(id)
if err != nil {
writeOrderError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) AdminHandoffRecords(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
items, err := h.service.HandoffRecordsAdmin(id)
if err != nil {
writeOrderError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func (h *Handler) Detail(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
@@ -462,6 +462,47 @@ func (r *Repository) ListForUser(userID uint64) ([]OrderDTO, error) {
return items, nil
}
func (r *Repository) ListAdmin() ([]OrderDTO, error) {
var rows []orderRow
err := r.adminQuery().
Order("o.id DESC").
Limit(200).
Scan(&rows).Error
if err != nil {
return nil, err
}
items := make([]OrderDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toDTO())
}
return items, nil
}
func (r *Repository) FindAdmin(orderID uint64) (*OrderDTO, error) {
var row orderRow
if err := r.adminQuery().Where("o.id = ?", orderID).First(&row).Error; err != nil {
return nil, err
}
dto := row.toDTO()
return &dto, nil
}
func (r *Repository) HandoffRecordsAdmin(orderID uint64) ([]HandoffRecordDTO, error) {
var order model.RentalOrder
if err := r.db.First(&order, orderID).Error; err != nil {
return nil, err
}
var records []model.HandoffRecord
if err := r.db.Where("order_id = ?", orderID).Order("id ASC").Find(&records).Error; err != nil {
return nil, err
}
items := make([]HandoffRecordDTO, 0, len(records))
for _, record := range records {
items = append(items, toHandoffDTO(record))
}
return items, nil
}
func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, error) {
var row orderRow
if err := r.baseQuery().
@@ -488,11 +529,21 @@ func (r *Repository) baseQuery() *gorm.DB {
Joins("JOIN game_accounts AS a ON a.id = o.account_id")
}
func (r *Repository) adminQuery() *gorm.DB {
return r.db.Table("rental_orders AS o").
Select("o.*, a.title, a.server_region, a.login_platform, owner.phone AS owner_phone, renter.phone AS renter_phone").
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
Joins("JOIN users AS owner ON owner.id = o.owner_id").
Joins("JOIN users AS renter ON renter.id = o.renter_id")
}
type orderRow struct {
model.RentalOrder
Title string
ServerRegion string
LoginPlatform string
OwnerPhone string
RenterPhone string
}
func (row orderRow) toDTO() OrderDTO {
@@ -503,6 +554,8 @@ func (row orderRow) toDTO() OrderDTO {
AccountID: row.AccountID,
OwnerID: row.OwnerID,
RenterID: row.RenterID,
OwnerPhone: row.OwnerPhone,
RenterPhone: row.RenterPhone,
Title: row.Title,
ServerRegion: row.ServerRegion,
LoginPlatform: row.LoginPlatform,
+21
View File
@@ -88,6 +88,27 @@ func (s *Service) ListForUser(userID uint64) ([]OrderDTO, error) {
return s.repo.ListForUser(userID)
}
func (s *Service) ListAdmin() ([]OrderDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.ListAdmin()
}
func (s *Service) FindAdmin(orderID uint64) (*OrderDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.FindAdmin(orderID)
}
func (s *Service) HandoffRecordsAdmin(orderID uint64) ([]HandoffRecordDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.HandoffRecordsAdmin(orderID)
}
func (s *Service) FindForUser(userID uint64, orderID uint64) (*OrderDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
+25
View File
@@ -23,3 +23,28 @@ type LedgerDTO struct {
Remark string `json:"remark"`
CreatedAt time.Time `json:"created_at"`
}
type AdminLedgerQuery struct {
UserID uint64
OrderID uint64
BizType string
Limit int
}
type AdminLedgerDTO struct {
ID uint64 `json:"id"`
LedgerNo string `json:"ledger_no"`
UserID uint64 `json:"user_id"`
UserPhone string `json:"user_phone"`
UserNickname string `json:"user_nickname"`
OrderID *uint64 `json:"order_id"`
OrderNo string `json:"order_no"`
Direction string `json:"direction"`
Amount float64 `json:"amount"`
BalanceAfter float64 `json:"balance_after"`
BalanceType string `json:"balance_type"`
BizType string `json:"biz_type"`
BizNo string `json:"biz_no"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"created_at"`
}
@@ -2,6 +2,7 @@ package wallet
import (
"errors"
"strconv"
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/pkg/response"
@@ -45,6 +46,49 @@ func (h *Handler) Ledger(c *gin.Context) {
response.OK(c, gin.H{"items": items})
}
func (h *Handler) AdminLedger(c *gin.Context) {
query, ok := parseAdminLedgerQuery(c)
if !ok {
return
}
items, err := h.service.AdminLedger(query)
if err != nil {
writeWalletError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func parseAdminLedgerQuery(c *gin.Context) (AdminLedgerQuery, bool) {
var query AdminLedgerQuery
if raw := c.Query("user_id"); raw != "" {
value, err := strconv.ParseUint(raw, 10, 64)
if err != nil || value == 0 {
response.BadRequest(c, "用户 ID 不正确")
return query, false
}
query.UserID = value
}
if raw := c.Query("order_id"); raw != "" {
value, err := strconv.ParseUint(raw, 10, 64)
if err != nil || value == 0 {
response.BadRequest(c, "订单 ID 不正确")
return query, false
}
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")
return query, true
}
func currentUserID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextUserID)
if !ok {
@@ -57,6 +57,37 @@ func (r *Repository) Ledger(userID uint64) ([]LedgerDTO, error) {
return items, nil
}
func (r *Repository) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
limit := query.Limit
if limit <= 0 || limit > 500 {
limit = 200
}
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,
wl.direction, wl.amount, wl.balance_after, wl.balance_type, wl.biz_type, wl.biz_no,
wl.remark, wl.created_at`).
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")
if query.UserID > 0 {
db = db.Where("wl.user_id = ?", query.UserID)
}
if query.OrderID > 0 {
db = db.Where("wl.order_id = ?", query.OrderID)
}
if query.BizType != "" {
db = db.Where("wl.biz_type = ?", query.BizType)
}
var items []AdminLedgerDTO
if err := db.Order("wl.id DESC").Limit(limit).Scan(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func AppendEntries(tx *gorm.DB, entries ...Entry) error {
for _, entry := range entries {
if entry.Amount <= 0 {
@@ -25,3 +25,10 @@ func (s *Service) Ledger(userID uint64) ([]LedgerDTO, error) {
}
return s.repo.Ledger(userID)
}
func (s *Service) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.AdminLedger(query)
}
+14
View File
@@ -6,6 +6,7 @@ import (
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/internal/modules/adminauth"
"hfb_sys/backend/internal/modules/admindashboard"
"hfb_sys/backend/internal/modules/adminuser"
"hfb_sys/backend/internal/modules/auth"
"hfb_sys/backend/internal/modules/dispute"
"hfb_sys/backend/internal/modules/listing"
@@ -51,6 +52,12 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
}
adminDashboardService := admindashboard.NewService(adminDashboardRepo)
adminDashboardHandler := admindashboard.NewHandler(adminDashboardService)
var adminUserRepo *adminuser.Repository
if deps.DB != nil {
adminUserRepo = adminuser.NewRepository(deps.DB)
}
adminUserService := adminuser.NewService(adminUserRepo)
adminUserHandler := adminuser.NewHandler(adminUserService)
userHandler := user.NewHandler(userRepo)
var realnameRepo *realname.Repository
if deps.DB != nil {
@@ -177,11 +184,18 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.GET("/me", adminAuthHandler.Me)
adminRoutes.POST("/auth/logout", adminAuthHandler.Logout)
adminRoutes.GET("/dashboard", adminDashboardHandler.Summary)
adminRoutes.GET("/users", adminUserHandler.List)
adminRoutes.POST("/users/:id/freeze", adminUserHandler.Freeze)
adminRoutes.POST("/users/:id/unfreeze", adminUserHandler.Unfreeze)
adminRoutes.GET("/orders", orderHandler.AdminList)
adminRoutes.GET("/orders/:id", orderHandler.AdminDetail)
adminRoutes.GET("/orders/:id/handoff-records", orderHandler.AdminHandoffRecords)
adminRoutes.GET("/listings/pending", listingHandler.ListPendingReview)
adminRoutes.POST("/listings/:id/approve", listingHandler.Approve)
adminRoutes.POST("/listings/:id/reject", listingHandler.Reject)
adminRoutes.GET("/disputes", disputeHandler.AdminList)
adminRoutes.POST("/disputes/:id/arbitrate", disputeHandler.AdminArbitrate)
adminRoutes.GET("/wallet/ledger", walletHandler.AdminLedger)
adminRoutes.GET("/system-configs", systemConfigHandler.List)
adminRoutes.PUT("/system-configs/:key", systemConfigHandler.Update)
}