Files
lijiaoqiao/supply-api/internal/httpapi/supply_api.go

844 lines
24 KiB
Go
Raw Permalink Normal View History

package httpapi
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"lijiaoqiao/supply-api/internal/audit"
"lijiaoqiao/supply-api/internal/domain"
"lijiaoqiao/supply-api/internal/storage"
)
// Supply API 处理器
type SupplyAPI struct {
accountService domain.AccountService
packageService domain.PackageService
settlementService domain.SettlementService
earningService domain.EarningService
idempotencyStore *storage.InMemoryIdempotencyStore
auditStore *audit.MemoryAuditStore
supplierID int64
now func() time.Time
}
func NewSupplyAPI(
accountService domain.AccountService,
packageService domain.PackageService,
settlementService domain.SettlementService,
earningService domain.EarningService,
idempotencyStore *storage.InMemoryIdempotencyStore,
auditStore *audit.MemoryAuditStore,
supplierID int64,
now func() time.Time,
) *SupplyAPI {
return &SupplyAPI{
accountService: accountService,
packageService: packageService,
settlementService: settlementService,
earningService: earningService,
idempotencyStore: idempotencyStore,
auditStore: auditStore,
supplierID: supplierID,
now: now,
}
}
func (a *SupplyAPI) Register(mux *http.ServeMux) {
// Supply Accounts
mux.HandleFunc("/api/v1/supply/accounts/verify", a.handleVerifyAccount)
mux.HandleFunc("/api/v1/supply/accounts", a.handleCreateAccount)
mux.HandleFunc("/api/v1/supply/accounts/", a.handleAccountActions)
// Supply Packages
mux.HandleFunc("/api/v1/supply/packages/draft", a.handleCreatePackageDraft)
mux.HandleFunc("/api/v1/supply/packages/batch-price", a.handleBatchUpdatePrice)
mux.HandleFunc("/api/v1/supply/packages/", a.handlePackageActions)
// Supply Billing
mux.HandleFunc("/api/v1/supply/billing", a.handleGetBilling)
mux.HandleFunc("/api/v1/supplier/billing", a.handleGetBilling) // 兼容别名
// Supply Settlements
mux.HandleFunc("/api/v1/supply/settlements/withdraw", a.handleWithdraw)
mux.HandleFunc("/api/v1/supply/settlements/", a.handleSettlementActions)
// Supply Earnings
mux.HandleFunc("/api/v1/supply/earnings/records", a.handleGetEarningRecords)
}
// ==================== Account Handlers ====================
type VerifyAccountRequest struct {
Provider string `json:"provider"`
AccountType string `json:"account_type"`
CredentialInput string `json:"credential_input"`
MinQuotaThreshold float64 `json:"min_quota_threshold,omitempty"`
}
func (a *SupplyAPI) handleVerifyAccount(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
defer r.Body.Close()
var req VerifyAccountRequest
if err := json.Unmarshal(body, &req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
result, err := a.accountService.Verify(r.Context(), a.supplierID,
domain.Provider(req.Provider),
domain.AccountType(req.AccountType),
req.CredentialInput)
if err != nil {
writeError(w, http.StatusUnprocessableEntity, "VERIFY_FAILED", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": result,
})
}
func (a *SupplyAPI) handleCreateAccount(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
requestID := r.Header.Get("X-Request-Id")
idempotencyKey := r.Header.Get("Idempotency-Key")
// 幂等检查
if idempotencyKey != "" {
if record, found := a.idempotencyStore.Get(idempotencyKey); found {
if record.Status == "succeeded" {
writeJSON(w, http.StatusOK, map[string]any{
"request_id": requestID,
"idempotent_replay": true,
"data": record.Response,
})
return
}
}
a.idempotencyStore.SetProcessing(idempotencyKey, 24*time.Hour)
}
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
defer r.Body.Close()
// 解析请求
var rawReq struct {
Provider string `json:"provider"`
AccountType string `json:"account_type"`
CredentialInput string `json:"credential_input"`
AccountAlias string `json:"account_alias"`
RiskAck bool `json:"risk_ack"`
}
if err := json.Unmarshal(body, &rawReq); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
createReq := &domain.CreateAccountRequest{
SupplierID: a.supplierID,
Provider: domain.Provider(rawReq.Provider),
AccountType: domain.AccountType(rawReq.AccountType),
Credential: rawReq.CredentialInput,
Alias: rawReq.AccountAlias,
RiskAck: rawReq.RiskAck,
}
account, err := a.accountService.Create(r.Context(), createReq)
if err != nil {
writeError(w, http.StatusUnprocessableEntity, "CREATE_FAILED", err.Error())
return
}
resp := map[string]any{
"account_id": account.ID,
"provider": account.Provider,
"account_type": account.AccountType,
"status": account.Status,
"created_at": account.CreatedAt,
}
// 保存幂等结果
if idempotencyKey != "" {
a.idempotencyStore.SetSuccess(idempotencyKey, resp, 24*time.Hour)
}
writeJSON(w, http.StatusCreated, map[string]any{
"request_id": requestID,
"data": resp,
})
}
func (a *SupplyAPI) handleAccountActions(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/supply/accounts/")
parts := strings.Split(path, "/")
if len(parts) < 2 {
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
return
}
accountID, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "invalid account_id")
return
}
action := parts[1]
switch action {
case "activate":
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handleActivateAccount(w, r, accountID)
case "suspend":
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handleSuspendAccount(w, r, accountID)
case "delete":
if r.Method != http.MethodDelete {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handleDeleteAccount(w, r, accountID)
case "audit-logs":
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handleAccountAuditLogs(w, r, accountID)
default:
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
}
}
func (a *SupplyAPI) handleActivateAccount(w http.ResponseWriter, r *http.Request, accountID int64) {
account, err := a.accountService.Activate(r.Context(), a.supplierID, accountID)
if err != nil {
if strings.Contains(err.Error(), "SUP_ACC") {
writeError(w, http.StatusConflict, "CONFLICT", err.Error())
} else {
writeError(w, http.StatusNotFound, "NOT_FOUND", err.Error())
}
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": map[string]any{
"account_id": account.ID,
"status": account.Status,
"updated_at": account.UpdatedAt,
},
})
}
func (a *SupplyAPI) handleSuspendAccount(w http.ResponseWriter, r *http.Request, accountID int64) {
account, err := a.accountService.Suspend(r.Context(), a.supplierID, accountID)
if err != nil {
if strings.Contains(err.Error(), "SUP_ACC") {
writeError(w, http.StatusConflict, "CONFLICT", err.Error())
} else {
writeError(w, http.StatusNotFound, "NOT_FOUND", err.Error())
}
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": map[string]any{
"account_id": account.ID,
"status": account.Status,
"updated_at": account.UpdatedAt,
},
})
}
func (a *SupplyAPI) handleDeleteAccount(w http.ResponseWriter, r *http.Request, accountID int64) {
err := a.accountService.Delete(r.Context(), a.supplierID, accountID)
if err != nil {
if strings.Contains(err.Error(), "SUP_ACC") {
writeError(w, http.StatusConflict, "CONFLICT", err.Error())
} else {
writeError(w, http.StatusNotFound, "NOT_FOUND", err.Error())
}
return
}
w.WriteHeader(http.StatusNoContent)
}
func (a *SupplyAPI) handleAccountAuditLogs(w http.ResponseWriter, r *http.Request, accountID int64) {
page := getQueryInt(r, "page", 1)
pageSize := getQueryInt(r, "page_size", 20)
events, err := a.auditStore.Query(r.Context(), audit.EventFilter{
TenantID: a.supplierID,
ObjectType: "supply_account",
ObjectID: accountID,
Limit: pageSize,
})
if err != nil {
writeError(w, http.StatusInternalServerError, "QUERY_FAILED", err.Error())
return
}
var items []map[string]any
for _, ev := range events {
items = append(items, map[string]any{
"event_id": ev.EventID,
"operator_id": ev.TenantID,
"tenant_id": ev.TenantID,
"object_type": ev.ObjectType,
"object_id": ev.ObjectID,
"action": ev.Action,
"request_id": ev.RequestID,
"created_at": ev.CreatedAt,
})
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": items,
"pagination": map[string]int{
"page": page,
"page_size": pageSize,
"total": len(items),
},
})
}
// ==================== Package Handlers ====================
func (a *SupplyAPI) handleCreatePackageDraft(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
defer r.Body.Close()
var req struct {
SupplyAccountID int64 `json:"supply_account_id"`
Model string `json:"model"`
TotalQuota float64 `json:"total_quota"`
PricePer1MInput float64 `json:"price_per_1m_input"`
PricePer1MOutput float64 `json:"price_per_1m_output"`
ValidDays int `json:"valid_days"`
MaxConcurrent int `json:"max_concurrent"`
RateLimitRPM int `json:"rate_limit_rpm"`
}
if err := json.Unmarshal(body, &req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
createReq := &domain.CreatePackageDraftRequest{
SupplierID: a.supplierID,
AccountID: req.SupplyAccountID,
Model: req.Model,
TotalQuota: req.TotalQuota,
PricePer1MInput: req.PricePer1MInput,
PricePer1MOutput: req.PricePer1MOutput,
ValidDays: req.ValidDays,
MaxConcurrent: req.MaxConcurrent,
RateLimitRPM: req.RateLimitRPM,
}
pkg, err := a.packageService.CreateDraft(r.Context(), a.supplierID, createReq)
if err != nil {
writeError(w, http.StatusUnprocessableEntity, "CREATE_FAILED", err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]any{
"request_id": getRequestID(r),
"data": map[string]any{
"package_id": pkg.ID,
"supply_account_id": pkg.SupplierID,
"model": pkg.Model,
"status": pkg.Status,
"total_quota": pkg.TotalQuota,
"available_quota": pkg.AvailableQuota,
"created_at": pkg.CreatedAt,
},
})
}
func (a *SupplyAPI) handlePackageActions(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/supply/packages/")
parts := strings.Split(path, "/")
if len(parts) < 1 {
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
return
}
// 批量调价
if len(parts) == 1 && parts[0] == "batch-price" {
a.handleBatchUpdatePrice(w, r)
return
}
packageID, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "invalid package_id")
return
}
if len(parts) < 2 {
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
return
}
action := parts[1]
switch action {
case "publish":
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handlePublishPackage(w, r, packageID)
case "pause":
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handlePausePackage(w, r, packageID)
case "unlist":
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handleUnlistPackage(w, r, packageID)
case "clone":
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handleClonePackage(w, r, packageID)
default:
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
}
}
func (a *SupplyAPI) handlePublishPackage(w http.ResponseWriter, r *http.Request, packageID int64) {
pkg, err := a.packageService.Publish(r.Context(), a.supplierID, packageID)
if err != nil {
if strings.Contains(err.Error(), "SUP_PKG") {
writeError(w, http.StatusConflict, "CONFLICT", err.Error())
} else {
writeError(w, http.StatusNotFound, "NOT_FOUND", err.Error())
}
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": map[string]any{
"package_id": pkg.ID,
"status": pkg.Status,
"updated_at": pkg.UpdatedAt,
},
})
}
func (a *SupplyAPI) handlePausePackage(w http.ResponseWriter, r *http.Request, packageID int64) {
pkg, err := a.packageService.Pause(r.Context(), a.supplierID, packageID)
if err != nil {
if strings.Contains(err.Error(), "SUP_PKG") {
writeError(w, http.StatusConflict, "CONFLICT", err.Error())
} else {
writeError(w, http.StatusNotFound, "NOT_FOUND", err.Error())
}
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": map[string]any{
"package_id": pkg.ID,
"status": pkg.Status,
"updated_at": pkg.UpdatedAt,
},
})
}
func (a *SupplyAPI) handleUnlistPackage(w http.ResponseWriter, r *http.Request, packageID int64) {
pkg, err := a.packageService.Unlist(r.Context(), a.supplierID, packageID)
if err != nil {
if strings.Contains(err.Error(), "SUP_PKG") {
writeError(w, http.StatusConflict, "CONFLICT", err.Error())
} else {
writeError(w, http.StatusNotFound, "NOT_FOUND", err.Error())
}
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": map[string]any{
"package_id": pkg.ID,
"status": pkg.Status,
"updated_at": pkg.UpdatedAt,
},
})
}
func (a *SupplyAPI) handleClonePackage(w http.ResponseWriter, r *http.Request, packageID int64) {
pkg, err := a.packageService.Clone(r.Context(), a.supplierID, packageID)
if err != nil {
writeError(w, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]any{
"request_id": getRequestID(r),
"data": map[string]any{
"package_id": pkg.ID,
"supply_account_id": pkg.SupplierID,
"model": pkg.Model,
"status": pkg.Status,
"created_at": pkg.CreatedAt,
},
})
}
func (a *SupplyAPI) handleBatchUpdatePrice(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
defer r.Body.Close()
var rawReq struct {
Items []struct {
PackageID int64 `json:"package_id"`
PricePer1MInput float64 `json:"price_per_1m_input"`
PricePer1MOutput float64 `json:"price_per_1m_output"`
} `json:"items"`
}
if err := json.Unmarshal(body, &rawReq); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
req := &domain.BatchUpdatePriceRequest{
Items: make([]domain.BatchPriceItem, len(rawReq.Items)),
}
for i, item := range rawReq.Items {
req.Items[i] = domain.BatchPriceItem{
PackageID: item.PackageID,
PricePer1MInput: item.PricePer1MInput,
PricePer1MOutput: item.PricePer1MOutput,
}
}
resp, err := a.packageService.BatchUpdatePrice(r.Context(), a.supplierID, req)
if err != nil {
writeError(w, http.StatusUnprocessableEntity, "BATCH_UPDATE_FAILED", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": resp,
})
}
// ==================== Billing Handlers ====================
func (a *SupplyAPI) handleGetBilling(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
startDate := r.URL.Query().Get("start_date")
endDate := r.URL.Query().Get("end_date")
summary, err := a.earningService.GetBillingSummary(r.Context(), a.supplierID, startDate, endDate)
if err != nil {
writeError(w, http.StatusInternalServerError, "QUERY_FAILED", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": summary,
})
}
// ==================== Settlement Handlers ====================
func (a *SupplyAPI) handleWithdraw(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
requestID := r.Header.Get("X-Request-Id")
idempotencyKey := r.Header.Get("Idempotency-Key")
// 幂等检查
if idempotencyKey != "" {
if record, found := a.idempotencyStore.Get(idempotencyKey); found {
if record.Status == "succeeded" {
writeJSON(w, http.StatusOK, map[string]any{
"request_id": requestID,
"idempotent_replay": true,
"data": record.Response,
})
return
}
}
a.idempotencyStore.SetProcessing(idempotencyKey, 72*time.Hour) // 提现类72h
}
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
defer r.Body.Close()
var req struct {
WithdrawAmount float64 `json:"withdraw_amount"`
PaymentMethod string `json:"payment_method"`
PaymentAccount string `json:"payment_account"`
SMSCode string `json:"sms_code"`
}
if err := json.Unmarshal(body, &req); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", err.Error())
return
}
withdrawReq := &domain.WithdrawRequest{
Amount: req.WithdrawAmount,
PaymentMethod: domain.PaymentMethod(req.PaymentMethod),
PaymentAccount: req.PaymentAccount,
SMSCode: req.SMSCode,
}
settlement, err := a.settlementService.Withdraw(r.Context(), a.supplierID, withdrawReq)
if err != nil {
if strings.Contains(err.Error(), "SUP_SET") {
writeError(w, http.StatusConflict, "WITHDRAW_FAILED", err.Error())
} else {
writeError(w, http.StatusUnprocessableEntity, "WITHDRAW_FAILED", err.Error())
}
return
}
resp := map[string]any{
"settlement_id": settlement.ID,
"settlement_no": settlement.SettlementNo,
"status": settlement.Status,
"total_amount": settlement.TotalAmount,
"net_amount": settlement.NetAmount,
"created_at": settlement.CreatedAt,
}
// 保存幂等结果
if idempotencyKey != "" {
a.idempotencyStore.SetSuccess(idempotencyKey, resp, 72*time.Hour)
}
writeJSON(w, http.StatusCreated, map[string]any{
"request_id": requestID,
"data": resp,
})
}
func (a *SupplyAPI) handleSettlementActions(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/supply/settlements/")
parts := strings.Split(path, "/")
if len(parts) < 2 {
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
return
}
settlementID, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "invalid settlement_id")
return
}
action := parts[1]
switch action {
case "cancel":
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handleCancelSettlement(w, r, settlementID)
case "statement":
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
a.handleGetStatement(w, r, settlementID)
default:
writeError(w, http.StatusNotFound, "NOT_FOUND", "route not found")
}
}
func (a *SupplyAPI) handleCancelSettlement(w http.ResponseWriter, r *http.Request, settlementID int64) {
settlement, err := a.settlementService.Cancel(r.Context(), a.supplierID, settlementID)
if err != nil {
if strings.Contains(err.Error(), "SUP_SET") {
writeError(w, http.StatusConflict, "CONFLICT", err.Error())
} else {
writeError(w, http.StatusNotFound, "NOT_FOUND", err.Error())
}
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": map[string]any{
"settlement_id": settlement.ID,
"status": settlement.Status,
"updated_at": settlement.UpdatedAt,
},
})
}
func (a *SupplyAPI) handleGetStatement(w http.ResponseWriter, r *http.Request, settlementID int64) {
settlement, err := a.settlementService.GetByID(r.Context(), a.supplierID, settlementID)
if err != nil {
writeError(w, http.StatusNotFound, "NOT_FOUND", err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": map[string]any{
"settlement_id": settlement.ID,
"file_name": fmt.Sprintf("statement_%s.pdf", settlement.SettlementNo),
"download_url": fmt.Sprintf("https://example.com/statements/%s.pdf", settlement.SettlementNo),
"expires_at": a.now().Add(1 * time.Hour),
},
})
}
// ==================== Earning Handlers ====================
func (a *SupplyAPI) handleGetEarningRecords(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "method not allowed")
return
}
startDate := r.URL.Query().Get("start_date")
endDate := r.URL.Query().Get("end_date")
page := getQueryInt(r, "page", 1)
pageSize := getQueryInt(r, "page_size", 20)
records, total, err := a.earningService.ListRecords(r.Context(), a.supplierID, startDate, endDate, page, pageSize)
if err != nil {
writeError(w, http.StatusInternalServerError, "QUERY_FAILED", err.Error())
return
}
var items []map[string]any
for _, record := range records {
items = append(items, map[string]any{
"record_id": record.ID,
"earnings_type": record.EarningsType,
"amount": record.Amount,
"status": record.Status,
"earned_at": record.EarnedAt,
})
}
writeJSON(w, http.StatusOK, map[string]any{
"request_id": getRequestID(r),
"data": items,
"pagination": map[string]int{
"page": page,
"page_size": pageSize,
"total": total,
},
})
}
// ==================== Helpers ====================
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func writeError(w http.ResponseWriter, status int, code, message string) {
writeJSON(w, status, map[string]any{
"request_id": "",
"error": map[string]any{
"code": code,
"message": message,
},
})
}
func getRequestID(r *http.Request) string {
if id := r.Header.Get("X-Request-Id"); id != "" {
return id
}
return r.Header.Get("X-Request-ID")
}
func getQueryInt(r *http.Request, key string, defaultVal int) int {
if val := r.URL.Query().Get(key); val != "" {
if intVal, err := strconv.Atoi(val); err == nil {
return intVal
}
}
return defaultVal
}