283 lines
8.9 KiB
Go
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)
|
|
}
|
|
}
|