479 lines
12 KiB
JavaScript
479 lines
12 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|