feat(http): 适配后端大写 envelope 与测试环境 BASE_URL

This commit is contained in:
ozh 2026-09-15 11:18:26 +08:00
parent fb66a9efe7
commit 9fbe9266eb
2 changed files with 101 additions and 8 deletions

View File

@ -2,7 +2,10 @@ import Taro from '@tarojs/taro'
import { store } from '@/store' import { store } from '@/store'
import { ApiError, BusinessError } from './errors' import { ApiError, BusinessError } from './errors'
const BASE_URL = '' // 后端就绪后填入 // 测试环境(内网,开发者工具需勾选「不校验合法域名」);正式域名为占位符,上线前必须替换(spec D2)
const BASE_URL = process.env.NODE_ENV === 'development'
? 'http://192.168.10.11:39101/api/biz'
: 'https://PROD_PLACEHOLDER/api/biz'
export interface RequestConfig { export interface RequestConfig {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
@ -11,10 +14,14 @@ export interface RequestConfig {
needAuth?: boolean // 默认 true;登录接口自身传 false needAuth?: boolean // 默认 true;登录接口自身传 false
} }
// 后端统一响应格式(大写 key、字符串状态码)
export interface ApiEnvelope<T> { export interface ApiEnvelope<T> {
code: number Code: string
data: T Message?: string
message?: string Content: T
RecordCount?: number
PageIndex?: number
PageSize?: number
} }
export async function request<T>( export async function request<T>(
@ -25,7 +32,7 @@ export async function request<T>(
const method = config.method ?? 'GET' const method = config.method ?? 'GET'
const timeout = config.timeout ?? 10000 const timeout = config.timeout ?? 10000
// token 注入(spec §3B):从 authSlice 读取 // token 注入:从 authSlice 读取
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...config.headers ...config.headers
@ -55,10 +62,10 @@ export async function request<T>(
} }
const body = res.data as ApiEnvelope<T> const body = res.data as ApiEnvelope<T>
if (body.code !== 0) { if (String(body.Code) !== '200') {
throw new BusinessError(body.message ?? '业务错误', body.code) throw new BusinessError(body.Message ?? '业务错误', String(body.Code ?? ''))
} }
return body.data return body.Content
} }
request.get = <T>(url: string, params?: Record<string, unknown>, config?: RequestConfig) => request.get = <T>(url: string, params?: Record<string, unknown>, config?: RequestConfig) =>

View File

@ -0,0 +1,86 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
// Taro.request 由各用例设置返回值(vi.hoisted 保证 mock 提升后仍可引用)
const mockRequest = vi.hoisted(() => vi.fn())
vi.mock('@tarojs/taro', () => ({ default: { request: mockRequest } }))
// client 依赖 store 取 token;此处只读,不触发 401 流程
vi.mock('@/store', () => ({
store: { getState: () => ({ auth: { token: 'unit-token' } }), dispatch: vi.fn() }
}))
import { request } from '@/http/client'
import { ApiError, BusinessError } from '@/http/errors'
const okBody = <T>(content: T) => ({
statusCode: 200,
data: { Code: '200', Message: '', Content: content, RecordCount: 0, PageIndex: 0, PageSize: 0 }
})
beforeEach(() => {
mockRequest.mockReset()
})
describe('client envelope 解析', () => {
it('Code=200 时解包返回 Content', async () => {
mockRequest.mockResolvedValue(okBody({ name: '张三' }))
const res = await request.get<{ name: string }>('/demo')
expect(res).toEqual({ name: '张三' })
// GET 参数经 data 透传(Taro 运行时序列化为 query string)
expect(mockRequest.mock.calls[0][0].data).toBeUndefined()
})
it('GET 参数放入 data 字段', async () => {
mockRequest.mockResolvedValue(okBody(null))
await request.get('/demo', { page: 1 })
expect(mockRequest.mock.calls[0][0].data).toEqual({ page: 1 })
})
it("Code='500' 抛 BusinessError(message + 字符串 code)", async () => {
mockRequest.mockResolvedValue({
statusCode: 200,
data: { Code: '500', Message: '手机号已绑定', Content: null }
})
await expect(request.get('/demo')).rejects.toMatchObject({
name: 'BusinessError',
message: '手机号已绑定',
code: '500'
})
await expect(request.get('/demo')).rejects.toBeInstanceOf(BusinessError)
})
it('HTTP 404 抛 ApiError 并带 statusCode', async () => {
mockRequest.mockResolvedValue({ statusCode: 404, data: 'Not Found' })
await expect(request.get('/demo')).rejects.toMatchObject({
name: 'ApiError',
message: 'HTTP 404',
statusCode: 404
})
})
it('Taro.request 异常(断网等)抛 ApiError', async () => {
mockRequest.mockRejectedValue(new Error('request:fail timeout'))
await expect(request.get('/demo')).rejects.toMatchObject({
name: 'ApiError',
message: 'request:fail timeout'
})
})
it('需鉴权请求注入 Bearer token 头', async () => {
mockRequest.mockResolvedValue(okBody(null))
await request.get('/demo')
expect(mockRequest.mock.calls[0][0].header.Authorization).toBe('Bearer unit-token')
})
it('needAuth: false 时不注入 Authorization', async () => {
mockRequest.mockResolvedValue(okBody(null))
await request.post('/demo', {}, { needAuth: false })
expect(mockRequest.mock.calls[0][0].header.Authorization).toBeUndefined()
})
it('BASE_URL 拼接:请求 url = BASE_URL + path', async () => {
mockRequest.mockResolvedValue(okBody(null))
await request.get('/thirdPlatform/wxDevLogin/miniapp/login')
expect(mockRequest.mock.calls[0][0].url).toMatch(/\/api\/biz\/thirdPlatform\/wxDevLogin\/miniapp\/login$/)
})
})