bm-bmt/api/bmt.js

479 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
}