diff --git a/backend/app/services/market_support.py b/backend/app/services/market_support.py index e8d7c507..0e65b6aa 100644 --- a/backend/app/services/market_support.py +++ b/backend/app/services/market_support.py @@ -708,6 +708,9 @@ def decorate_allowed_actions( return data if module == 'presign' and detail: parent_writable = status.lower() in {'', 'draft', 'active'} + data['transferAllowedActions'] = ( + ['create-transfer'] if parent_writable else [] + ) transfers = [] for transfer in data.get('transfers') or []: transfer_data = dict(transfer) diff --git a/backend/tests/test_market_presign_mobile_actions.py b/backend/tests/test_market_presign_mobile_actions.py new file mode 100644 index 00000000..714eaefd --- /dev/null +++ b/backend/tests/test_market_presign_mobile_actions.py @@ -0,0 +1,28 @@ +import pytest + +from app.services.market_support import decorate_allowed_actions + + +@pytest.mark.parametrize('status', ['', 'draft', 'active']) +def test_presign_detail_authorizes_transfer_creation_for_writable_parent(status): + result = decorate_allowed_actions( + 'presign', + {'orderStatus': status, 'transfers': []}, + scope_kind='specialist', + detail=True, + ) + + assert result['allowedActions'] == ['edit', 'convert', 'delete'] + assert result['transferAllowedActions'] == ['create-transfer'] + + +@pytest.mark.parametrize('status', ['void', 'converted', 'unexpected']) +def test_presign_detail_denies_transfer_creation_for_locked_parent(status): + result = decorate_allowed_actions( + 'presign', + {'orderStatus': status, 'transfers': []}, + scope_kind='manager', + detail=True, + ) + + assert result['transferAllowedActions'] == [] diff --git a/mobile-oa-h5/src/api/market.js b/mobile-oa-h5/src/api/market.js index 86530803..7fb1778a 100644 --- a/mobile-oa-h5/src/api/market.js +++ b/mobile-oa-h5/src/api/market.js @@ -19,11 +19,30 @@ function assertContext(context) { } } +export function normalizeMarketEntityId(id, { rejectPathSeparators = false } = {}) { + if (id == null) throw new Error('业务实体 ID 无效') + const normalized = String(id).trim() + if ( + !normalized || + normalized.length > 64 || + normalized === '.' || + normalized === '..' || + /[\u0000-\u001f\u007f-\u009f]/.test(normalized) || + (rejectPathSeparators && /[\\/]/.test(normalized)) + ) { + throw new Error('业务实体 ID 无效') + } + return normalized +} + export function buildMarketResourcePath(context, id) { assertContext(context) const base = `/mini/market/${context.marketRole}/${RESOURCE_BY_MODULE[context.module]}` - const normalizedId = String(id ?? '').trim() - return normalizedId ? `${base}/${encodeURIComponent(normalizedId)}` : base + if (arguments.length < 2) return base + const normalizedId = normalizeMarketEntityId(id, { + rejectPathSeparators: context.module === 'presign' + }) + return `${base}/${encodeURIComponent(normalizedId)}` } export function buildMarketFormOptionsPath(context) { @@ -36,11 +55,14 @@ export function buildMarketAttachmentPath(context) { return `/mini/market/${context.marketRole}/${context.module}/attachments` } -export function buildMarketActionPath(context, action, id) { +export function buildMarketActionPath(context, action, id, data = {}) { assertContext(context) const roleBase = `/mini/market/${context.marketRole}` - const encodedId = encodeURIComponent(String(id ?? '').trim()) - if (context.module === 'presign' && encodedId) { + const encodeId = () => encodeURIComponent(normalizeMarketEntityId(id, { + rejectPathSeparators: context.module === 'presign' + })) + if (context.module === 'presign') { + const encodedId = encodeId() if (action === 'delete') return buildMarketResourcePath(context, id) if (action === 'convert') return `${buildMarketResourcePath(context, id)}/convert` if (action === 'create-transfer') { @@ -56,15 +78,27 @@ export function buildMarketActionPath(context, action, id) { } if (context.module === 'payment') { if (action === 'register-transfer') return `${roleBase}/payment-transfers` - if (['edit-transfer', 'submit', 'withdraw'].includes(action) && encodedId) { + if (['edit-transfer', 'submit', 'withdraw'].includes(action)) { + const encodedId = encodeId() const suffix = action === 'edit-transfer' ? '' : `/${action}` return `${roleBase}/payment-transfers/${encodedId}${suffix}` } } - if (context.module === 'sample' && ['storage-cert', 'insurance'].includes(action) && encodedId) { + if (context.module === 'protocol' && ['audit', 'delete'].includes(action)) { + encodeId() + const source = String(data?.source || '').trim() + if (!['cell_storage', 'research_donation'].includes(source)) { + throw new Error('协议业务来源无效') + } + const suffix = action === 'audit' ? '/audit' : '' + return `${buildMarketResourcePath(context, id)}${suffix}?source=${encodeURIComponent(source)}` + } + if (context.module === 'sample' && ['storage-cert', 'insurance'].includes(action)) { + encodeId() return `${buildMarketResourcePath(context, id)}/${action}` } - if (context.module === 'freight' && ['link', 'unlink'].includes(action) && encodedId) { + if (context.module === 'freight' && ['link', 'unlink'].includes(action)) { + encodeId() return `${buildMarketResourcePath(context, id)}/${action}` } throw new Error('不支持的市场业务操作') @@ -97,9 +131,14 @@ export async function fetchMarketFormOptions(context, params, { signal } = {}) { export async function saveMarketRecord(context, { id, data }, { signal } = {}) { const { request } = await import('../utils/http.js') + const normalizedId = id == null || id === '' + ? '' + : normalizeMarketEntityId(id, { rejectPathSeparators: context?.module === 'presign' }) return request({ - url: buildMarketResourcePath(context, id), - method: id ? 'PUT' : 'POST', + url: normalizedId + ? buildMarketResourcePath(context, normalizedId) + : buildMarketResourcePath(context), + method: normalizedId ? 'PUT' : 'POST', data, signal }) @@ -114,10 +153,13 @@ export function marketActionMethod(action) { export async function performMarketAction(context, action, { id, data } = {}, { signal } = {}) { const { request } = await import('../utils/http.js') + const requestData = context.module === 'protocol' && ['audit', 'delete'].includes(action) + ? Object.fromEntries(Object.entries(data || {}).filter(([key]) => key !== 'source')) + : data return request({ - url: buildMarketActionPath(context, action, id), + url: buildMarketActionPath(context, action, id, data), method: marketActionMethod(action), - data, + data: action === 'delete' && context.module === 'protocol' ? undefined : requestData, signal }) } diff --git a/mobile-oa-h5/src/market/action-policy.js b/mobile-oa-h5/src/market/action-policy.js index 3869d7ab..7605a718 100644 --- a/mobile-oa-h5/src/market/action-policy.js +++ b/mobile-oa-h5/src/market/action-policy.js @@ -31,17 +31,17 @@ const PRESIGN_TOP_ACTIONS = new Set([ 'create', 'edit', 'convert', - 'delete', - 'create-transfer' + 'delete' ]) const PRESIGN_ORDER_ACTIONS = new Set([ 'edit', 'convert', - 'delete', - 'create-transfer' + 'delete' ]) +const PRESIGN_TRANSFER_CREATE_ACTIONS = new Set(['create-transfer']) + const PRESIGN_TRANSFER_ACTION_MAP = Object.freeze({ edit: 'edit-presign-transfer', delete: 'delete-transfer', @@ -123,9 +123,23 @@ export function collectAllowedActions(module, role, detail) { return { ...metadata, code } }) .filter(Boolean) + const createTransfer = (Array.isArray(detail?.transferAllowedActions) + ? detail.transferAllowedActions + : []) + .map((action) => { + const code = typeof action === 'string' + ? action + : String(action?.code || action?.action || action?.key || '').trim() + if (!PRESIGN_TRANSFER_CREATE_ACTIONS.has(code) || !frontendWhitelist.has(code)) return null + if (typeof action === 'string') return action + const { key: _key, action: _action, ...metadata } = action + return { ...metadata, code } + }) + .filter(Boolean) return [ ...nested, - ...top + ...top, + ...createTransfer ] } if (module !== 'payment') { diff --git a/mobile-oa-h5/src/market/api-contract.test.js b/mobile-oa-h5/src/market/api-contract.test.js index b422bbfb..cea53027 100644 --- a/mobile-oa-h5/src/market/api-contract.test.js +++ b/mobile-oa-h5/src/market/api-contract.test.js @@ -8,8 +8,11 @@ import { createPresignTransfer, deletePresignOrder, deletePresignTransfer, + fetchMarketDetail, marketActionMethod, buildMarketResourcePath, + normalizeMarketEntityId, + saveMarketRecord, submitPresignTransfer, updatePresignTransfer, withdrawPresignTransfer @@ -53,16 +56,16 @@ test('maps every presign write action to the fixed backend route with encoded id '/mini/market/specialist/presign-orders/order%201' ) assert.equal( - buildMarketActionPath(presign, 'create-transfer', 'order/1'), - '/mini/market/specialist/presign-orders/order%2F1/transfers' + buildMarketActionPath(presign, 'create-transfer', 'order 1'), + '/mini/market/specialist/presign-orders/order%201/transfers' ) assert.equal( buildMarketActionPath(presign, 'edit-presign-transfer', 'transfer 1'), '/mini/market/specialist/presign-transfers/transfer%201' ) assert.equal( - buildMarketActionPath(presign, 'delete-transfer', 'transfer/1'), - '/mini/market/specialist/presign-transfers/transfer%2F1' + buildMarketActionPath(presign, 'delete-transfer', 'transfer 1'), + '/mini/market/specialist/presign-transfers/transfer%201' ) assert.equal( buildMarketActionPath(presign, 'submit-transfer', 'transfer 1'), @@ -74,6 +77,51 @@ test('maps every presign write action to the fixed backend route with encoded id ) }) +test('normalizes safe entity ids and rejects dangerous presign path segments', () => { + const presign = { marketRole: 'specialist', module: 'presign' } + const uuid = '123e4567-e89b-12d3-a456-426614174000' + + assert.equal(normalizeMarketEntityId(` ${uuid} `), uuid) + assert.equal(buildMarketResourcePath(presign, ' o 1 '), '/mini/market/specialist/presign-orders/o%201') + assert.equal(buildMarketActionPath(presign, 'submit-transfer', 't 1'), '/mini/market/specialist/presign-transfers/t%201/submit') + assert.equal( + buildMarketActionPath({ marketRole: 'specialist', module: 'protocol' }, 'delete', 'p/1', { source: 'cell_storage' }), + '/mini/market/specialist/protocols/p%2F1?source=cell_storage' + ) + + for (const id of [null, undefined, '', ' ', '.', '..', 'a/control\u0000id', 'a/control\u009fid', 'x'.repeat(65)]) { + assert.throws(() => normalizeMarketEntityId(id), /ID/) + } + assert.throws(() => buildMarketResourcePath(presign, undefined), /ID/) + for (const id of ['order/1', 'order\\1']) { + assert.throws(() => buildMarketResourcePath(presign, id), /ID/) + assert.throws(() => buildMarketActionPath(presign, 'delete', id), /ID/) + } +}) + +test('invalid detail, save, and presign action ids fail before an HTTP request', async () => { + const presign = { marketRole: 'specialist', module: 'presign' } + const originalFetch = globalThis.fetch + let calls = 0 + globalThis.fetch = async () => { + calls += 1 + throw new Error('fetch should not run') + } + try { + await assert.rejects(fetchMarketDetail(presign, ' '), /ID/) + await assert.rejects(saveMarketRecord(presign, { id: ' ', data: {} }), /ID/) + await assert.rejects(deletePresignOrder(presign, '..'), /ID/) + await assert.rejects(createPresignTransfer(presign, 'order/1', {}), /ID/) + await assert.rejects(updatePresignTransfer(presign, 'transfer\\1', {}), /ID/) + await assert.rejects(deletePresignTransfer(presign, ''), /ID/) + await assert.rejects(submitPresignTransfer(presign, null), /ID/) + await assert.rejects(withdrawPresignTransfer(presign, 'x'.repeat(65)), /ID/) + assert.equal(calls, 0) + } finally { + globalThis.fetch = originalFetch + } +}) + test('uses the backend HTTP method for each presign action', () => { assert.equal(marketActionMethod('delete'), 'DELETE') assert.equal(marketActionMethod('create-transfer'), 'POST') diff --git a/mobile-oa-h5/src/market/market-signal.test.js b/mobile-oa-h5/src/market/market-signal.test.js new file mode 100644 index 00000000..a45f689b --- /dev/null +++ b/mobile-oa-h5/src/market/market-signal.test.js @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { fetchMarketList, saveMarketRecord } from '../api/market.js' + +const context = { marketRole: 'specialist', module: 'presign' } + +test('market wrappers pass the caller abort signal through to fetch', async () => { + const originalFetch = globalThis.fetch + const originalSessionStorage = globalThis.sessionStorage + const calls = [] + globalThis.sessionStorage = { getItem: () => 'test-token' } + globalThis.fetch = async (url, init) => { + calls.push({ url, init }) + return { + status: 200, + json: async () => ({ code: 200, data: { ok: true } }) + } + } + + try { + const listController = new AbortController() + const saveController = new AbortController() + await fetchMarketList(context, { page: 1 }, { signal: listController.signal }) + await saveMarketRecord(context, { id: '', data: { motherName: '新增' } }, { + signal: saveController.signal + }) + await saveMarketRecord(context, { id: ' o 1 ', data: { motherName: '张女士' } }, { + signal: saveController.signal + }) + + assert.equal(calls[0].init.signal, listController.signal) + assert.equal(calls[1].init.signal, saveController.signal) + assert.equal(calls[1].init.method, 'POST') + assert.match(calls[1].url, /presign-orders$/) + assert.equal(calls[2].init.signal, saveController.signal) + assert.equal(calls[2].init.method, 'PUT') + assert.match(calls[2].url, /presign-orders\/o%201$/) + } finally { + globalThis.fetch = originalFetch + if (originalSessionStorage === undefined) delete globalThis.sessionStorage + else globalThis.sessionStorage = originalSessionStorage + } +}) diff --git a/mobile-oa-h5/src/market/module-actions.test.js b/mobile-oa-h5/src/market/module-actions.test.js index 5c02dda2..9f8dee5f 100644 --- a/mobile-oa-h5/src/market/module-actions.test.js +++ b/mobile-oa-h5/src/market/module-actions.test.js @@ -128,7 +128,7 @@ test('presign actions never exceed the backend authorization set', () => { 'finance-confirm', 'admin-only' ]), - ['delete', 'create-transfer'] + ['delete'] ) assert.equal( intersectAllowedActions('presign', 'specialist', ['edit']).includes('delete'), @@ -149,7 +149,7 @@ test('presign order actions reject transfer-only backend action codes', () => { 'submit-transfer', 'withdraw-transfer' ]), - ['create', 'edit', 'convert', 'delete', 'create-transfer'] + ['create', 'edit', 'convert', 'delete'] ) }) @@ -183,14 +183,17 @@ test('presign transfer actions accept only raw transfer authorization codes with }) test('collects only backend-authorized presign order and nested transfer actions', () => { - const actions = collectAllowedActions('presign', 'manager', { + const detail = { allowedActions: ['create', 'edit', 'convert', 'delete', 'withdraw-transfer', 'admin-only'], + transferAllowedActions: ['create-transfer', 'admin-only'], transfers: [ { id: 'draft', allowedActions: ['edit', 'delete', 'submit', 'finance-confirm'] }, { id: 'pending', allowedActions: ['withdraw'] }, { id: 'confirmed', allowedActions: [] } ] - }) + } + const before = structuredClone(detail) + const actions = collectAllowedActions('presign', 'manager', detail) assert.deepEqual( actions.map((item) => [typeof item === 'string' ? item : item.code, item.transferId]), @@ -201,9 +204,29 @@ test('collects only backend-authorized presign order and nested transfer actions ['withdraw-transfer', 'pending'], ['edit', undefined], ['convert', undefined], - ['delete', undefined] + ['delete', undefined], + ['create-transfer', undefined] ] ) assert.equal(actions.some((item) => item.code === 'finance-confirm'), false) assert.equal(actions.some((item) => item.code === 'admin-only'), false) + assert.deepEqual(detail, before) +}) + +test('create transfer requires the dedicated backend authorization field', () => { + assert.deepEqual( + collectAllowedActions('presign', 'specialist', { + allowedActions: ['edit', 'create-transfer'], + transfers: [] + }), + ['edit'] + ) + assert.deepEqual( + collectAllowedActions('presign', 'specialist', { + allowedActions: [], + transferAllowedActions: ['create-transfer'], + transfers: [] + }), + ['create-transfer'] + ) }) diff --git a/mobile-oa-h5/src/market/presign/presign-form-state.js b/mobile-oa-h5/src/market/presign/presign-form-state.js index dde4aef4..e3a4e58c 100644 --- a/mobile-oa-h5/src/market/presign/presign-form-state.js +++ b/mobile-oa-h5/src/market/presign/presign-form-state.js @@ -92,14 +92,17 @@ export function isPresignTransferLocked(transfer, orderOrStatus) { } function attachmentUrls(transfer) { + if (Array.isArray(transfer?.attachments)) { + return transfer.attachments + .filter((file) => !file?._uploading && !file?.failed && file?.status !== 'failed') + .map((file) => String(file?.fileUrl || file?.url || '').trim()) + .filter(Boolean) + .join(',') + } if (typeof transfer?.transferAttachments === 'string') { return transfer.transferAttachments } - return (Array.isArray(transfer?.attachments) ? transfer.attachments : []) - .filter((file) => !file?._uploading && !file?.failed && file?.status !== 'failed') - .map((file) => String(file?.fileUrl || file?.url || '').trim()) - .filter(Boolean) - .join(',') + return '' } export function buildPresignTransferPayload(transfer = {}) { diff --git a/mobile-oa-h5/src/market/presign/presign-form-state.test.js b/mobile-oa-h5/src/market/presign/presign-form-state.test.js index 343f9092..5c3ebff7 100644 --- a/mobile-oa-h5/src/market/presign/presign-form-state.test.js +++ b/mobile-oa-h5/src/market/presign/presign-form-state.test.js @@ -171,3 +171,31 @@ test('builds a transfer payload without ids, status, timestamps, or finance fiel remark: '' }) }) + +test('uses the current attachment array instead of a stale backend URL string', () => { + assert.equal(buildPresignTransferPayload({ + transferAttachments: 'old.pdf,removed.pdf', + attachments: [ + { fileUrl: 'old.pdf' }, + { url: 'added.pdf' }, + { fileUrl: 'uploading.pdf', _uploading: true }, + { fileUrl: 'failed.pdf', status: 'failed' } + ] + }).transferAttachments, 'old.pdf,added.pdf') + + assert.equal(buildPresignTransferPayload({ + transferAttachments: 'old.pdf', + attachments: [{ fileUrl: 'added.pdf' }] + }).transferAttachments, 'added.pdf') +}) + +test('an explicit empty attachment array deletes every transfer attachment', () => { + assert.equal(buildPresignTransferPayload({ + transferAttachments: 'old.pdf', + attachments: [] + }).transferAttachments, '') + + assert.equal(buildPresignTransferPayload({ + transferAttachments: 'fallback.pdf' + }).transferAttachments, 'fallback.pdf') +}) diff --git a/mobile-oa-h5/src/market/presign/presign-presenter.js b/mobile-oa-h5/src/market/presign/presign-presenter.js index 0aa4e967..f0e865d5 100644 --- a/mobile-oa-h5/src/market/presign/presign-presenter.js +++ b/mobile-oa-h5/src/market/presign/presign-presenter.js @@ -42,14 +42,42 @@ export function formatPresignDateTime(value) { return `${date} ${match[2]}:${match[3]}${seconds}` } +function decimalParts(value) { + if (value == null) return null + if (typeof value === 'number' && !Number.isFinite(value)) return null + const text = String(value).trim() + if (!text) return null + const match = /^([+-]?)(?:(\d+)(?:\.(\d*))?|\.(\d+))(?:[eE]([+-]?\d+))?$/.exec(text) + if (!match) return null + const exponent = Number(match[5] || 0) + if (!Number.isSafeInteger(exponent) || Math.abs(exponent) > 100) return null + const integerDigits = match[2] || '' + const fractionDigits = match[3] ?? match[4] ?? '' + const digits = `${integerDigits}${fractionDigits}` || '0' + const decimalPosition = integerDigits.length + exponent + const whole = decimalPosition <= 0 + ? '0' + : `${digits.slice(0, decimalPosition)}${'0'.repeat(Math.max(0, decimalPosition - digits.length))}` + const fraction = decimalPosition <= 0 + ? `${'0'.repeat(-decimalPosition)}${digits}` + : digits.slice(decimalPosition) + return { + negative: match[1] === '-', + whole: whole.replace(/^0+(?=\d)/, '') || '0', + fraction + } +} + export function formatPresignMoney(value) { - if (value == null || String(value).trim() === '') return '-' - const amount = Number(value) - if (!Number.isFinite(amount)) return '-' - return `¥${new Intl.NumberFormat('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }).format(amount)}` + const parts = decimalParts(value) + if (!parts) return '-' + const fraction = parts.fraction.padEnd(3, '0') + let cents = BigInt(parts.whole) * 100n + BigInt(fraction.slice(0, 2)) + if (fraction[2] >= '5') cents += 1n + const integer = (cents / 100n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + const decimals = (cents % 100n).toString().padStart(2, '0') + const sign = parts.negative && cents !== 0n ? '-' : '' + return `¥${sign}${integer}.${decimals}` } export function presignStatusLabel(value) { @@ -62,15 +90,19 @@ export function presignStatusTone(value) { } export function presignCardIndex(page, pageSize, position) { - const normalizedPage = Number(page) - const normalizedPageSize = Number(pageSize) - const normalizedPosition = Number(position) - if ( - !Number.isInteger(normalizedPage) || normalizedPage < 1 || - !Number.isInteger(normalizedPageSize) || normalizedPageSize < 1 || - !Number.isInteger(normalizedPosition) || normalizedPosition < 1 - ) { - return '-' + const positiveInteger = (value) => { + if (typeof value === 'bigint') return value > 0n ? value : null + if (typeof value === 'number') { + return Number.isSafeInteger(value) && value > 0 ? BigInt(value) : null + } + const text = String(value ?? '').trim() + if (!/^\+?\d{1,256}$/.test(text)) return null + const parsed = BigInt(text) + return parsed > 0n ? parsed : null } - return String((normalizedPage - 1) * normalizedPageSize + normalizedPosition) + const normalizedPage = positiveInteger(page) + const normalizedPageSize = positiveInteger(pageSize) + const normalizedPosition = positiveInteger(position) + if (normalizedPage == null || normalizedPageSize == null || normalizedPosition == null) return '-' + return ((normalizedPage - 1n) * normalizedPageSize + normalizedPosition).toString() } diff --git a/mobile-oa-h5/src/market/presign/presign-presenter.test.js b/mobile-oa-h5/src/market/presign/presign-presenter.test.js index 61b8978c..18104fae 100644 --- a/mobile-oa-h5/src/market/presign/presign-presenter.test.js +++ b/mobile-oa-h5/src/market/presign/presign-presenter.test.js @@ -27,6 +27,9 @@ test('returns a safe placeholder for null, empty, or invalid dates', () => { test('formats finite amounts with a currency prefix and two decimals', () => { assert.equal(formatPresignMoney(28800), '¥28,800.00') assert.equal(formatPresignMoney('0'), '¥0.00') + assert.equal(formatPresignMoney('-12.345'), '¥-12.35') + assert.equal(formatPresignMoney('9999999999999999.99'), '¥9,999,999,999,999,999.99') + assert.equal(formatPresignMoney('1.005'), '¥1.01') for (const value of [null, undefined, '', 'invalid', Number.NaN, Number.POSITIVE_INFINITY]) { assert.equal(formatPresignMoney(value), '-') } @@ -46,5 +49,10 @@ test('uses the Web presign status labels and mobile semantic tones', () => { test('computes a stable one-based sequence across pages', () => { assert.equal(presignCardIndex(1, 10, 1), '1') assert.equal(presignCardIndex(2, 10, 3), '13') + assert.equal( + presignCardIndex('900719925474099300000', '10000000000000000000', '7'), + '9007199254740992999990000000000000000007' + ) assert.equal(presignCardIndex('bad', 10, 1), '-') + assert.equal(presignCardIndex(Number.MAX_SAFE_INTEGER + 1, 10, 1), '-') }) diff --git a/mobile-oa-h5/src/utils/http.js b/mobile-oa-h5/src/utils/http.js index 45d1a8e4..e1e89067 100644 --- a/mobile-oa-h5/src/utils/http.js +++ b/mobile-oa-h5/src/utils/http.js @@ -1,6 +1,6 @@ import { getToken } from './auth.js' -const API_BASE = (import.meta.env.VITE_API_BASE || '/api').replace(/\/$/, '') +const API_BASE = (import.meta.env?.VITE_API_BASE || '/api').replace(/\/$/, '') function unwrap(body) { if (body == null || typeof body !== 'object') { @@ -10,7 +10,7 @@ function unwrap(body) { throw new Error((body.message && String(body.message).trim()) || '请求失败') } -export async function request({ url, method = 'GET', data, headers = {} }) { +export async function request({ url, method = 'GET', data, headers = {}, signal }) { const path = url.startsWith('/') ? url : `/${url}` const full = `${API_BASE}${path}` const token = getToken() @@ -19,6 +19,7 @@ export async function request({ url, method = 'GET', data, headers = {} }) { } const init = { method: method.toUpperCase(), + signal, headers: { Authorization: `Bearer ${token}`, ...headers