feat(pickup): 新增管理员线下提号功能

- 新建 admin_pickups 表独立于 rental_orders,不污染订单状态机与财务统计口径
- 提号中/已完成/已取消三态:创建锁定账号,完成时给卖家钱包加可用余额并下架 listing,取消解锁账号
- 完成时 listing 置 completed,复用现有 listingLockedForOwnerMutation 拦截再上架
- 管理端提号管理页(列表/创建/完成/取消/可提号账号搜索)+ 卖家端提号记录页
- 财务仪表盘新增线下提号独立统计块,与正常订单口径分离
- 钱包流水 label、状态标签、菜单入口同步补齐
- 优化卖家服务悬浮菜单为一行四个,尺寸与配色对齐买家服务
This commit is contained in:
yml2213
2026-07-03 19:04:35 +08:00
parent f5a1630a27
commit 9ef0843eb0
22 changed files with 1649 additions and 13 deletions
+168
View File
@@ -0,0 +1,168 @@
package pickup
import (
"errors"
"net/http"
"strings"
"hfb_sys/backend/internal/auditlog"
"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}
}
// Create 创建提号订单(管理员)
func (h *Handler) Create(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
var req CreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "参数格式错误")
return
}
item, err := h.service.Create(c.Request.Context(), req, adminID, auditMeta(c))
if err != nil {
writePickupError(c, err)
return
}
response.OK(c, item)
}
// Complete 完成提号结算(管理员)
func (h *Handler) Complete(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
id, ok := parseID(c)
if !ok {
return
}
var req CompleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "结算金额不正确")
return
}
item, err := h.service.Complete(c.Request.Context(), id, req, adminID, auditMeta(c))
if err != nil {
writePickupError(c, err)
return
}
response.OK(c, item)
}
// Cancel 取消提号(管理员)
func (h *Handler) Cancel(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
id, ok := parseID(c)
if !ok {
return
}
var req CancelRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "取消原因不能为空")
return
}
if err := h.service.Cancel(c.Request.Context(), id, req.Reason, adminID, auditMeta(c)); err != nil {
writePickupError(c, err)
return
}
response.OK(c, gin.H{"cancelled": true})
}
// List 提号订单列表(管理员)
func (h *Handler) List(c *gin.Context) {
query := parseAdminQuery(c)
result, err := h.service.ListAdmin(c.Request.Context(), query)
if err != nil {
writePickupError(c, err)
return
}
response.OK(c, result)
}
// Detail 提号订单详情(管理员)
func (h *Handler) Detail(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
item, err := h.service.FindByID(c.Request.Context(), id)
if err != nil {
writePickupError(c, err)
return
}
response.OK(c, item)
}
// AvailableListings 可提号的 listing 搜索(管理员)
func (h *Handler) AvailableListings(c *gin.Context) {
keyword := strings.TrimSpace(c.Query("keyword"))
page, pageSize := parsePagination(c)
result, err := h.service.ListAvailableListings(c.Request.Context(), keyword, page, pageSize)
if err != nil {
writePickupError(c, err)
return
}
response.OK(c, result)
}
// ListForSeller 卖家自己的提号记录
func (h *Handler) ListForSeller(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
query := parseSellerQuery(c)
result, err := h.service.ListForSeller(c.Request.Context(), userID, query)
if err != nil {
writePickupError(c, err)
return
}
response.OK(c, result)
}
func writePickupError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "数据库未连接")
case errors.Is(err, ErrListingUnavailable):
response.BadRequest(c, "账号不可用或已在交易中")
case errors.Is(err, ErrDuplicatePickup):
response.BadRequest(c, "该账号已有进行中的提号订单")
case errors.Is(err, ErrPickupNotFound):
response.Error(c, http.StatusNotFound, "pickup_not_found", "提号订单不存在")
case errors.Is(err, ErrPickupNotPickingUp):
response.BadRequest(c, "提号订单状态不允许该操作")
case errors.Is(err, ErrInvalidAmount):
response.BadRequest(c, "结算金额不正确")
default:
response.Error(c, http.StatusInternalServerError, "pickup_error", err.Error())
}
}
func auditMeta(c *gin.Context) auditlog.Meta {
return auditlog.Meta{
IP: c.ClientIP(),
UserAgent: c.GetHeader("User-Agent"),
RequestID: middleware.GetRequestID(c),
}
}