/** * BMT 签约相关接口 * 状态查询 / 前置校验 / 发起签约 / 图片上传 / OCR 识别 / 补充信息提交 * * 与 api/assets.js 保持同一套请求约定: * { status: 200, data: {...} } => 返回 data,否则抛出 { message } */ import serviceConfig from "../config/service"; import request from "../utils/request"; import { getCurrentWebviewToken } from "../utils/webview-token"; function createError(message, raw) { return { message: message || "接口请求失败", raw: raw, }; } function isSuccessPayload(payload) { if (!payload || typeof payload !== "object") { return false; } const statusCode = payload.code !== undefined ? Number(payload.code) : Number(payload.status); return statusCode === 200; } function unwrapPayload(payload, fallbackMessage) { if (isSuccessPayload(payload)) { return payload.data; } if (payload && typeof payload === "object") { throw createError( payload.msg || payload.message || fallbackMessage || "接口请求失败", payload, ); } throw createError(fallbackMessage || "接口返回异常", payload); } async function fetchPayload(options, fallbackMessage) { try { const payload = await request(options); return unwrapPayload(payload, fallbackMessage); } catch (error) { if (error && error.message) { throw error; } throw createError(fallbackMessage || "接口请求失败", error); } } function createRequestOptions(baseOptions, requestOptions) { return Object.assign({}, baseOptions, requestOptions || {}); } function pickFirstValue(source, keys) { if (!source || typeof source !== "object") { return ""; } for (let i = 0; i < keys.length; i += 1) { const value = source[keys[i]]; if (value !== undefined && value !== null && value !== "") { return value; } } return ""; } /** 接口返回体可能存在 info / data / result 一层或多层嵌套,统一摊平 */ function normalizeBmtSource(payload) { if (!payload || typeof payload !== "object") { return {}; } let merged = {}; ["info", "data", "result"].forEach(function (key) { const nested = payload[key]; if (nested && typeof nested === "object" && !Array.isArray(nested)) { merged = Object.assign(merged, nested); } }); return Object.assign(merged, payload); } function buildAuthHeader() { const token = getCurrentWebviewToken(); if (!token) { return {}; } const bearerToken = "Bearer " + token; return { "Authori-zation": bearerToken, Authorization: bearerToken, }; } const UPLOAD_MAX_SIZE = 2 * 1024 * 1024; // 2MB 以内不压缩 const COMPRESS_MAX_SIDE = 800; // 压缩后最长边 const COMPRESS_QUALITY = 0.8; // 起始压缩质量 const COMPRESS_MIN_QUALITY = 0.1; // 最低压缩质量 const COMPRESS_MAX_KB = 500; // 压缩目标大小 const UPLOAD_FIELD_NAME = "pics"; // 后端接收文件的字段名(与参考实现一致) // #ifdef H5 /** H5:用原生 canvas 压缩,按质量递减直到小于 COMPRESS_MAX_KB */ function compressImageForH5(filePath) { return new Promise(function (resolve, reject) { if (typeof document === "undefined") { reject(new Error("当前环境不支持图片压缩")); return; } const img = new Image(); img.src = filePath; img.onload = function () { let width = img.width; let height = img.height; if (width > height) { if (width > COMPRESS_MAX_SIDE) { height = height * (COMPRESS_MAX_SIDE / width); width = COMPRESS_MAX_SIDE; } } else if (height > COMPRESS_MAX_SIDE) { width = width * (COMPRESS_MAX_SIDE / height); height = COMPRESS_MAX_SIDE; } const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); const compress = function (quality) { const dataUrl = canvas.toDataURL("image/jpeg", quality); const sizeKb = (dataUrl.length * 3) / 4 / 1024; if (sizeKb > COMPRESS_MAX_KB && quality > COMPRESS_MIN_QUALITY) { return compress(quality - 0.05); } return dataUrl; }; resolve(compress(COMPRESS_QUALITY)); }; img.onerror = function () { reject(new Error("图片读取失败")); }; }); } // #endif // #ifndef H5 /** 小程序 / App:优先用 uni.compressImage 压缩 */ function compressImageForNative(filePath) { return new Promise(function (resolve) { if (typeof uni.compressImage !== "function") { resolve(filePath); return; } uni.compressImage({ src: filePath, quality: Math.round(COMPRESS_QUALITY * 100), success: function (result) { resolve(result && result.tempFilePath ? result.tempFilePath : filePath); }, fail: function () { resolve(filePath); }, }); }); } // #endif /** 上传前压缩:2MB 以内原样返回,超出则压缩;压缩失败退回原文件,不阻断上传 */ async function prepareUploadFile(filePath, fileSize) { const size = Number(fileSize || 0); if (size > 0 && size <= UPLOAD_MAX_SIZE) { return filePath; } try { // #ifdef H5 return await compressImageForH5(filePath); // #endif // #ifndef H5 return await compressImageForNative(filePath); // #endif } catch (error) { console.warn("[BMT上传] 图片压缩失败,改用原图", error); return filePath; } } /** * 上传认证图片(身份证) * 上传前先压缩(参考 uploadImageChange:2MB 以内直传,超出压到 800px / 500KB 以内); * 响应解析只要 HTTP 2xx 且能取到图片地址就算成功,不依赖业务 status/code * @param {string} filePath 本地临时路径 * @param {{ size?: number, header?: object }} options * @returns {Promise<{url: string}>} */ export async function uploadBmtImage(filePath, options) { const path = String(filePath || "").trim(); if (!path) { throw createError("图片上传失败"); } const fileSize = Number((options && options.size) || 0); const uploadPath = await prepareUploadFile(path, fileSize); console.log( "[BMT上传] 原始大小", fileSize, "上传来源", String(uploadPath).slice(0, 48), ); const header = Object.assign( { "Form-type": "gzh", // #ifdef MP "Content-Type": "multipart/form-data", // #endif }, buildAuthHeader(), (options && options.header) || {}, ); const url = serviceConfig.BASE_URL + serviceConfig.ENDPOINTS.bmtUploadImage; return new Promise(function (resolve, reject) { uni.uploadFile({ url: url, filePath: uploadPath, name: UPLOAD_FIELD_NAME, formData: { filename: UPLOAD_FIELD_NAME, }, header: header, success: function (response) { const statusCode = Number(response && response.statusCode); console.log("[BMT上传] 响应", statusCode, response && response.data); if (!(statusCode >= 200 && statusCode < 300)) { reject( createError("图片上传失败(HTTP " + statusCode + ")", response), ); return; } let payload = response && response.data; if (typeof payload === "string") { try { payload = JSON.parse(payload); } catch (error) { // 有些实现直接返回图片地址字符串 const text = String(response.data || "").trim(); if (/^https?:\/\//i.test(text)) { resolve({ url: text }); return; } reject(createError("图片上传失败", response)); return; } } if ( typeof payload === "string" && /^https?:\/\//i.test(payload.trim()) ) { resolve({ url: payload.trim() }); return; } // url 可能落在 payload / payload.data / payload.data.data 任意一层 const layers = []; let cursor = payload; for (let i = 0; i < 3 && cursor && typeof cursor === "object"; i += 1) { layers.push(cursor); cursor = cursor.data; } for (let i = 0; i < layers.length; i += 1) { const found = pickFirstValue(layers[i], [ "url", "src", "path", "file_url", "media_url", "image_url", ]); if (found) { resolve({ url: String(found) }); return; } } reject(createError("图片上传失败:响应中未找到图片地址", payload)); }, fail: function (error) { console.error("[BMT上传] 失败", error); reject(createError("图片上传失败", error)); }, }); }); } /** * BMT 签约状态查询 * 关键字段:sign_info_complete(0 待补充 / 1 已完善)、contract_status(0 待签约)、 * contract_type(0 签约新协议 / 1 更新协议)、tips(签约提示文案) */ export async function fetchBmtContractStatus(requestOptions) { const data = await fetchPayload( createRequestOptions( { url: serviceConfig.ENDPOINTS.bmtContractStatus, }, requestOptions, ), "签约状态查询失败", ); return normalizeBmtSource(data); } /** BMT 签约前置校验 */ export async function checkBmtSignCondition(payload, requestOptions) { const data = await fetchPayload( createRequestOptions( { url: serviceConfig.ENDPOINTS.bmtSignCheck, method: "GET", data: payload || {}, }, requestOptions, ), "签约前置校验失败", ); return normalizeBmtSource(data); } /** * BMT 发起签约 * 注意:该接口用业务码 status === 0 表示成功,与项目通用的 status/code === 200 约定不同, * 所以这里不走 fetchPayload 的通用解包,单独解析,返回 { status, message, saasShortLink }。 */ export async function startBmtSigning(payload, requestOptions) { let raw; try { raw = await request( createRequestOptions( { url: serviceConfig.ENDPOINTS.bmtSignStart, method: "POST", data: payload || {}, }, requestOptions, ), ); } catch (error) { throw createError((error && error.message) || "发起签约失败", error); } const source = raw && typeof raw === "object" ? raw : {}; // 字段可能落在 raw / raw.data / raw.data.data 任意一层,逐层收集 const layers = []; let cursor = source; for (let i = 0; i < 3 && cursor && typeof cursor === "object"; i += 1) { layers.push(cursor); cursor = cursor.data; } const pickText = (keys) => { for (let i = 0; i < layers.length; i += 1) { for (let j = 0; j < keys.length; j += 1) { const value = layers[i][keys[j]]; if (value !== undefined && value !== null && value !== "") { return String(value); } } } return ""; }; // status:优先 data 层的业务状态,其次顶层 status,最后顶层 code let statusValue; for (let i = layers.length - 1; i >= 1; i -= 1) { if (layers[i].status !== undefined && layers[i].status !== null) { statusValue = layers[i].status; break; } } if (statusValue === undefined && source.status !== undefined) { statusValue = source.status; } if (statusValue === undefined) { statusValue = source.code; } return { status: Number(statusValue), message: pickText(["msg", "message"]), saasShortLink: pickText(["saasShortLink", "saas_short_link"]), raw: source, }; } /** 图片 OCR 识别:type = idcard_ocr(身份证) */ export async function fetchBmtOcrResult(payload, requestOptions) { return fetchPayload( createRequestOptions( { url: serviceConfig.ENDPOINTS.bmtOcrImage, method: "POST", data: payload || {}, }, requestOptions, ), "图片识别失败", ); } /** 提交补充签约信息:/api/jz_contract/update_user_sign_info */ export async function submitBmtSignInfo(payload, requestOptions) { const data = await fetchPayload( createRequestOptions( { url: serviceConfig.ENDPOINTS.bmtUpdateUserSignInfo, method: "POST", data: payload || {}, }, requestOptions, ), "提交失败", ); return normalizeBmtSource(data); }