283 lines
8.9 KiB
Go

package payment
import (
"errors"
"math"
"strconv"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/walletclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
walletv1 "hyapp.local/api/proto/wallet/v1"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const usdtMicroUnit int64 = 1_000_000
type Handler struct {
wallet walletclient.Client
audit shared.OperationLogger
}
func New(wallet walletclient.Client, audit shared.OperationLogger) *Handler {
return &Handler{wallet: wallet, audit: audit}
}
func (h *Handler) ListRechargeBills(c *gin.Context) {
options := shared.ListOptions(c)
resp, err := h.wallet.ListRechargeBills(c.Request.Context(), &walletv1.ListRechargeBillsRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
UserId: queryInt64(c, "user_id", "userId"),
SellerUserId: queryInt64(c, "seller_user_id", "sellerUserId"),
RegionId: queryInt64(c, "region_id", "regionId"),
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
Status: options.Status,
Keyword: options.Keyword,
StartAtMs: queryInt64(c, "start_at_ms", "startAtMs"),
EndAtMs: queryInt64(c, "end_at_ms", "endAtMs"),
Page: int32(options.Page),
PageSize: int32(options.PageSize),
})
if err != nil {
response.ServerError(c, "获取账单列表失败")
return
}
items := make([]rechargeBillDTO, 0, len(resp.GetBills()))
for _, item := range resp.GetBills() {
items = append(items, rechargeBillFromProto(item))
}
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
func (h *Handler) ListRechargeProducts(c *gin.Context) {
options := shared.ListOptions(c)
resp, err := h.wallet.ListAdminRechargeProducts(c.Request.Context(), &walletv1.ListAdminRechargeProductsRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
Status: strings.TrimSpace(firstQuery(c, "status")),
Platform: strings.TrimSpace(firstQuery(c, "platform")),
RegionId: queryInt64(c, "region_id", "regionId"),
Keyword: options.Keyword,
Page: int32(options.Page),
PageSize: int32(options.PageSize),
})
if err != nil {
writeWalletError(c, err, "获取内购配置失败")
return
}
items := make([]rechargeProductDTO, 0, len(resp.GetProducts()))
for _, item := range resp.GetProducts() {
items = append(items, rechargeProductFromProto(item))
}
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
func (h *Handler) CreateRechargeProduct(c *gin.Context) {
var request rechargeProductRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.BadRequest(c, "内购配置参数不正确")
return
}
amountMicro, err := parseUSDTMicro(request.AmountUSDT, request.AmountUSDTMicro)
if err != nil {
response.BadRequest(c, "金额USDT不正确")
return
}
resp, err := h.wallet.CreateRechargeProduct(c.Request.Context(), &walletv1.CreateRechargeProductRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
AmountMicro: amountMicro,
CoinAmount: request.CoinAmount,
ProductName: strings.TrimSpace(request.ProductName),
Description: strings.TrimSpace(request.Description),
Platform: strings.TrimSpace(request.Platform),
RegionIds: request.RegionIDs,
Enabled: request.Enabled,
OperatorUserId: actorID(c),
})
if err != nil {
writeWalletError(c, err, "创建内购配置失败")
return
}
item := rechargeProductFromProto(resp.GetProduct())
shared.OperationLogWithResourceID(c, h.audit, "create-recharge-product", "wallet_recharge_products", strconv.FormatInt(item.ProductID, 10), "success", item.ProductCode)
response.Created(c, item)
}
func (h *Handler) UpdateRechargeProduct(c *gin.Context) {
productID, ok := productID(c)
if !ok {
return
}
var request rechargeProductRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.BadRequest(c, "内购配置参数不正确")
return
}
amountMicro, err := parseUSDTMicro(request.AmountUSDT, request.AmountUSDTMicro)
if err != nil {
response.BadRequest(c, "金额USDT不正确")
return
}
resp, err := h.wallet.UpdateRechargeProduct(c.Request.Context(), &walletv1.UpdateRechargeProductRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
ProductId: productID,
AmountMicro: amountMicro,
CoinAmount: request.CoinAmount,
ProductName: strings.TrimSpace(request.ProductName),
Description: strings.TrimSpace(request.Description),
Platform: strings.TrimSpace(request.Platform),
RegionIds: request.RegionIDs,
Enabled: request.Enabled,
OperatorUserId: actorID(c),
})
if err != nil {
writeWalletError(c, err, "修改内购配置失败")
return
}
item := rechargeProductFromProto(resp.GetProduct())
shared.OperationLogWithResourceID(c, h.audit, "update-recharge-product", "wallet_recharge_products", strconv.FormatInt(item.ProductID, 10), "success", item.ProductCode)
response.OK(c, item)
}
func (h *Handler) DeleteRechargeProduct(c *gin.Context) {
productID, ok := productID(c)
if !ok {
return
}
_, err := h.wallet.DeleteRechargeProduct(c.Request.Context(), &walletv1.DeleteRechargeProductRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
ProductId: productID,
OperatorUserId: actorID(c),
})
if err != nil {
writeWalletError(c, err, "删除内购配置失败")
return
}
shared.OperationLogWithResourceID(c, h.audit, "delete-recharge-product", "wallet_recharge_products", strconv.FormatInt(productID, 10), "success", "")
response.OK(c, gin.H{"deleted": true})
}
type rechargeProductRequest struct {
AmountUSDT string `json:"amountUsdt"`
AmountUSDTMicro int64 `json:"amountUsdtMicro"`
CoinAmount int64 `json:"coinAmount"`
ProductName string `json:"productName"`
Description string `json:"description"`
Platform string `json:"platform"`
RegionIDs []int64 `json:"regionIds"`
Enabled bool `json:"enabled"`
}
func queryInt64(c *gin.Context, keys ...string) int64 {
value := strings.TrimSpace(firstQuery(c, keys...))
if value == "" {
return 0
}
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil || parsed < 0 {
return 0
}
return parsed
}
func firstQuery(c *gin.Context, keys ...string) string {
for _, key := range keys {
if value := c.Query(key); value != "" {
return value
}
}
return ""
}
func productID(c *gin.Context) (int64, bool) {
value := strings.TrimSpace(c.Param("product_id"))
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil || parsed <= 0 {
response.BadRequest(c, "内购配置 ID 不正确")
return 0, false
}
return parsed, true
}
func actorID(c *gin.Context) int64 {
return int64(shared.ActorFromContext(c).UserID)
}
func parseUSDTMicro(amountText string, amountMicro int64) (int64, error) {
if amountMicro > 0 {
return amountMicro, nil
}
raw := strings.TrimSpace(amountText)
if raw == "" {
return 0, errors.New("amount is required")
}
if strings.HasPrefix(raw, "+") {
raw = strings.TrimPrefix(raw, "+")
}
parts := strings.Split(raw, ".")
if len(parts) > 2 || parts[0] == "" {
return 0, errors.New("amount format is invalid")
}
if !allDigits(parts[0]) {
return 0, errors.New("amount integer part is invalid")
}
whole, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil || whole > math.MaxInt64/usdtMicroUnit {
return 0, errors.New("amount is too large")
}
var fraction int64
if len(parts) == 2 {
if parts[1] == "" || len(parts[1]) > 6 || !allDigits(parts[1]) {
return 0, errors.New("amount fraction part is invalid")
}
padded := parts[1] + strings.Repeat("0", 6-len(parts[1]))
fraction, err = strconv.ParseInt(padded, 10, 64)
if err != nil {
return 0, errors.New("amount fraction part is invalid")
}
}
if whole > (math.MaxInt64-fraction)/usdtMicroUnit {
return 0, errors.New("amount is too large")
}
value := whole*usdtMicroUnit + fraction
if value <= 0 {
return 0, errors.New("amount must be positive")
}
return value, nil
}
func allDigits(value string) bool {
for _, char := range value {
if char < '0' || char > '9' {
return false
}
}
return value != ""
}
func writeWalletError(c *gin.Context, err error, fallback string) {
message := fallback
if st, ok := status.FromError(err); ok && strings.TrimSpace(st.Message()) != "" {
message = st.Message()
}
switch status.Code(err) {
case codes.InvalidArgument, codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
response.BadRequest(c, message)
case codes.NotFound:
response.NotFound(c, message)
default:
response.ServerError(c, fallback)
}
}