159 lines
7.6 KiB
Go
159 lines
7.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
const (
|
|
reasonMalformedRequest = "MALFORMED_REQUEST"
|
|
reasonMethodNotAllowed = "METHOD_NOT_ALLOWED"
|
|
reasonForbidden = "FORBIDDEN"
|
|
reasonInternal = "INTERNAL"
|
|
)
|
|
|
|
type apiError struct {
|
|
Code int `json:"code"`
|
|
Reason string `json:"reason"`
|
|
Message string `json:"message"`
|
|
Details map[string]any `json:"details,omitempty"`
|
|
RequestID string `json:"request_id,omitempty"`
|
|
ServerTime string `json:"server_time"`
|
|
}
|
|
|
|
var apiErrorMessages = map[string]string{
|
|
"MALFORMED_REQUEST": "请求参数格式错误",
|
|
"METHOD_NOT_ALLOWED": "当前请求方式不支持",
|
|
"AUTH_REQUIRED": "登录已失效,请重新登录",
|
|
"FORBIDDEN": "当前账户无权执行此操作",
|
|
"INTERNAL": "服务异常,请稍后重试",
|
|
"SERVICE_UNAVAILABLE": "服务暂不可用,请稍后重试",
|
|
"USERNAME_INVALID_FORMAT": "用户名格式不正确",
|
|
"PHONE_INVALID_FORMAT": "手机号码格式不正确",
|
|
"PASSWORD_INVALID_FORMAT": "密码格式不正确",
|
|
"REFERRAL_CODE_INVALID": "邀请码无效,请检查后重新输入",
|
|
"PRIVACY_POLICY_NOT_ACCEPTED": "请先阅读并同意用户协议和隐私政策",
|
|
"ACCOUNT_ALREADY_REGISTERED": "该账户或手机号已经注册",
|
|
"INVALID_CREDENTIALS": "手机号或密码错误",
|
|
"RATE_LIMITED": "操作过于频繁,请稍后再试",
|
|
"AVATAR_ATTACHMENT_INVALID": "头像图片无效,请重新上传",
|
|
"CURRENT_PASSWORD_INVALID": "当前登录密码不正确",
|
|
"LOGIN_PASSWORD_INVALID": "登录密码不正确",
|
|
"NEW_PASSWORD_INVALID": "新密码格式不正确",
|
|
"OLD_TRADE_PASSWORD_REQUIRED": "请输入原支付密码",
|
|
"OLD_TRADE_PASSWORD_INVALID": "原支付密码不正确",
|
|
"TRADE_PASSWORD_INVALID": "支付密码不正确",
|
|
"TRADE_PASSWORD_NOT_SET": "请先设置支付密码",
|
|
"ACCOUNT_NOT_FOUND": "账户不存在",
|
|
"ACCOUNT_CHANGED": "账户信息已发生变化,请重试",
|
|
"CATEGORY_ID_INVALID": "商品分类参数不正确",
|
|
"PRODUCT_ID_INVALID": "商品参数不正确",
|
|
"PRODUCT_NOT_FOUND": "商品不存在或已下架",
|
|
"PAGE_INVALID": "页码参数不正确",
|
|
"PAGE_SIZE_INVALID": "每页数量参数不正确",
|
|
"IMAGE_INVALID": "图片格式或大小不符合要求",
|
|
"HALL_ID_INVALID": "任务大厅参数不正确",
|
|
"ORDER_ID_INVALID": "订单参数不正确",
|
|
"ORDER_STATUS_INVALID": "订单状态参数不正确",
|
|
"EVIDENCE_INVALID": "任务凭证不符合要求",
|
|
"TASK_DISABLED": "领取功能暂未开放",
|
|
"OUTSIDE_BUSINESS_HOURS": "当前不在营业时间内",
|
|
"INSUFFICIENT_BALANCE": "可用余额不足",
|
|
"DAILY_LIMIT_REACHED": "今日领取次数已用完",
|
|
"LEVEL_NOT_ELIGIBLE": "当前会员等级不满足领取条件",
|
|
"ACCOUNT_RESTRICTED": "账户状态受限,请联系客服",
|
|
"PENDING_ORDER_EXISTS": "存在未完成的任务,请先处理",
|
|
"NO_TASK_AVAILABLE": "当前暂无可领取任务",
|
|
"ORDER_NOT_FOUND": "任务订单不存在",
|
|
"ORDER_STATE_INVALID": "当前任务状态不允许此操作",
|
|
"ORDER_EXPIRED": "该任务已超时",
|
|
"RESOURCE_NOT_FOUND": "请求的内容不存在",
|
|
"CUSTOMER_SERVICE_NOT_FOUND": "客服已下线或不存在",
|
|
"CUSTOMER_SERVICE_NOT_BOUND": "客服账号尚未配置",
|
|
"IM_RELATION_PENDING": "好友申请已发送,请等待客服确认",
|
|
"IM_RELATION_NOT_ALLOWED": "当前无法与该客服建立会话",
|
|
"IM_SERVICE_UNAVAILABLE": "客服系统暂不可用,请稍后重试",
|
|
"ACCOUNT_TYPE_INVALID": "收款账户类型不正确",
|
|
"PAYMENT_ACCOUNT_FIELDS_INVALID": "收款账户信息填写不完整或格式不正确",
|
|
"CURRENCY_INVALID": "币种参数不正确",
|
|
"WITHDRAWAL_AMOUNT_INVALID": "取出金额不符合当前规则",
|
|
"RECHARGE_AMOUNT_INVALID": "储入金额不符合当前规则",
|
|
"RECHARGE_REMARK_INVALID": "储入备注格式不正确",
|
|
"RECHARGE_VOUCHER_INVALID": "储入凭证无效,请重新上传",
|
|
"IDEMPOTENCY_KEY_INVALID": "请求标识无效,请刷新后重试",
|
|
"LEDGER_FILTER_INVALID": "账变记录筛选条件不正确",
|
|
"WITHDRAWAL_STATUS_INVALID": "取出状态参数不正确",
|
|
"RECHARGE_STATUS_INVALID": "储入状态参数不正确",
|
|
"RECHARGE_ORDER_ID_INVALID": "储入订单参数不正确",
|
|
"WITHDRAWAL_ORDER_ID_INVALID": "取出订单参数不正确",
|
|
"PAYMENT_ACCOUNT_EXISTS": "该收款账户已经存在",
|
|
"PAYMENT_ACCOUNT_INVALID": "收款账户未核验或不可用",
|
|
"WITHDRAWAL_DISABLED": "取出功能暂未开放",
|
|
"WITHDRAWAL_CHANNEL_UNAVAILABLE": "当前没有可用的取出通道",
|
|
"RECHARGE_CHANNEL_UNAVAILABLE": "当前没有可用的储入通道",
|
|
"WITHDRAWAL_DAILY_LIMIT_REACHED": "今日取出次数已用完",
|
|
"WITHDRAWAL_ORDER_NOT_FOUND": "取出订单不存在",
|
|
"RECHARGE_ORDER_NOT_FOUND": "储入订单不存在",
|
|
"IDEMPOTENCY_CONFLICT": "请勿重复提交不同的请求",
|
|
"QR_ATTACHMENT_INVALID": "二维码图片无效,请重新上传",
|
|
"BANNER_POSITION_INVALID": "轮播图位置参数不正确",
|
|
"CONTENT_KEY_INVALID": "内容页面参数不正确",
|
|
}
|
|
|
|
func writeSuccess(w http.ResponseWriter, r *http.Request, data any) {
|
|
writeJSON(w, r, http.StatusOK, map[string]any{
|
|
"code": 0, "data": data, "request_id": requestID(r),
|
|
"server_time": time.Now().UTC().Format(time.RFC3339Nano),
|
|
})
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, r *http.Request, status, code int, reason, message string, details map[string]any) {
|
|
writeJSON(w, r, status, apiError{Code: code, Reason: reason, Message: localizedAPIErrorMessage(reason, message), Details: details,
|
|
RequestID: requestID(r), ServerTime: time.Now().UTC().Format(time.RFC3339Nano)})
|
|
}
|
|
|
|
func localizedAPIErrorMessage(reason, message string) string {
|
|
if localized := apiErrorMessages[reason]; localized != "" {
|
|
return localized
|
|
}
|
|
if strings.IndexFunc(message, func(r rune) bool { return unicode.Is(unicode.Han, r) }) >= 0 {
|
|
return message
|
|
}
|
|
return "请求失败,请稍后重试"
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, r *http.Request, status int, value any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
if requestID := requestID(r); requestID != "" {
|
|
w.Header().Set("X-Request-ID", requestID)
|
|
}
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(value)
|
|
}
|
|
|
|
func requestID(r *http.Request) string {
|
|
return r.Header.Get("X-Request-ID")
|
|
}
|
|
|
|
func decodeBody(w http.ResponseWriter, r *http.Request, dst any) bool {
|
|
defer r.Body.Close()
|
|
decoder := json.NewDecoder(r.Body)
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(dst); err != nil {
|
|
writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", nil)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func methodNotAllowed(w http.ResponseWriter, r *http.Request, allowed ...string) {
|
|
if len(allowed) == 0 {
|
|
allowed = []string{http.MethodPost}
|
|
}
|
|
w.Header().Set("Allow", strings.Join(allowed, ", "))
|
|
writeError(w, r, http.StatusMethodNotAllowed, 30005, reasonMethodNotAllowed, "method not allowed", nil)
|
|
}
|