feat: 对接微信安全检测
This commit is contained in:
parent
80ac4e815e
commit
f6bd6b4684
@ -1,5 +1,5 @@
|
|||||||
import { getCache } from '@/utils/cache';
|
import { getCache } from '@/utils/cache';
|
||||||
import { request, HOST, TENANT_ID } from '@/utils/request';
|
import { request, HOST, TENANT_ID, handleUnauthorized, HttpError } from '@/utils/request';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传图片到后端,返回 { url, ... }
|
* 上传图片到后端,返回 { url, ... }
|
||||||
@ -19,10 +19,14 @@ export const uploadFile = (filePath: string): Promise<{ url: string; [key: strin
|
|||||||
success: (res) => {
|
success: (res) => {
|
||||||
try {
|
try {
|
||||||
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
||||||
if (data.code === 200) {
|
if (data.code === 200 || data.code === 0) {
|
||||||
resolve(data.data);
|
resolve(data.data);
|
||||||
|
} else if (Number(data.code) === 401) {
|
||||||
|
const msg401 = data.message || data.msg || '登录已失效,请重新登录';
|
||||||
|
handleUnauthorized(msg401);
|
||||||
|
reject(new HttpError(msg401, 401, data));
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(data.message || '上传失败'));
|
reject(new Error(data.message || data.msg || '上传失败'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(new Error('上传返回格式异常'));
|
reject(new Error('上传返回格式异常'));
|
||||||
@ -33,15 +37,37 @@ export const uploadFile = (filePath: string): Promise<{ url: string; [key: strin
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface MediaAuditRecord {
|
||||||
|
id?: string;
|
||||||
|
recordId?: string;
|
||||||
|
titleId?: string;
|
||||||
|
bizId?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
mediaUrl?: string;
|
||||||
|
url?: string;
|
||||||
|
/** pass | review | risky */
|
||||||
|
suggest?: string;
|
||||||
|
/** 100 正常;20001 时政;20002 色情;20006 违法犯罪;21000 其他 */
|
||||||
|
label?: number;
|
||||||
|
status?: number | string;
|
||||||
|
auditStatus?: number | string;
|
||||||
|
checkStatus?: number | string;
|
||||||
|
pass?: boolean;
|
||||||
|
auditPass?: boolean;
|
||||||
|
auditResult?: string;
|
||||||
|
resultMsg?: string;
|
||||||
|
message?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查图片是否合规
|
* 提交图片内容安全检测(异步,结果需通过查询接口获取)
|
||||||
*/
|
*/
|
||||||
export const checkImage = (data: {
|
export const checkImage = (data: {
|
||||||
bizId: string;
|
openid: string;
|
||||||
bizType: number;
|
recordId: string;
|
||||||
|
titleId: string;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
scene: number;
|
|
||||||
recordId?: string;
|
|
||||||
}) =>
|
}) =>
|
||||||
request({
|
request({
|
||||||
url: '/applet/media/checkImage',
|
url: '/applet/media/checkImage',
|
||||||
@ -49,3 +75,25 @@ export const checkImage = (data: {
|
|||||||
data,
|
data,
|
||||||
silent: true,
|
silent: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按试卷记录查询全部图片审核结果
|
||||||
|
*/
|
||||||
|
export const listMediaByRecordId = (recordId: string) =>
|
||||||
|
request<MediaAuditRecord[]>({
|
||||||
|
url: '/applet/media/listByRecordId',
|
||||||
|
method: 'GET',
|
||||||
|
params: { recordId },
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按试卷记录 + 题目查询图片审核结果
|
||||||
|
*/
|
||||||
|
export const getMediaByRecordAndTitle = (recordId: string, titleId: string) =>
|
||||||
|
request<MediaAuditRecord | MediaAuditRecord[]>({
|
||||||
|
url: '/applet/media/getByRecordAndTitle',
|
||||||
|
method: 'GET',
|
||||||
|
params: { recordId, titleId },
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|||||||
@ -3,11 +3,16 @@ import '@vingogo/uni-ui/lib/style.css';
|
|||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import pinia from './state/index';
|
import pinia from './state/index';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
import { useUserStore } from '@/state/modules/user';
|
||||||
|
import { registerClearSessionHandler } from '@/utils/session';
|
||||||
import './style/index.css';
|
import './style/index.css';
|
||||||
|
|
||||||
export function createApp() {
|
export function createApp() {
|
||||||
const app = createSSRApp(App);
|
const app = createSSRApp(App);
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
|
registerClearSessionHandler(() => {
|
||||||
|
void useUserStore().clear();
|
||||||
|
});
|
||||||
app.use(router);
|
app.use(router);
|
||||||
return {
|
return {
|
||||||
app,
|
app,
|
||||||
|
|||||||
@ -269,19 +269,30 @@
|
|||||||
<text>请将答案写在纸上,拍照上传(最多3张)</text>
|
<text>请将答案写在纸上,拍照上传(最多3张)</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="picList.length" class="pic-list">
|
<view v-if="displayPicList.length" class="pic-list">
|
||||||
<view
|
<view
|
||||||
v-for="(pic, pIdx) in picList"
|
v-for="(pic, pIdx) in displayPicList"
|
||||||
:key="pIdx"
|
:key="pIdx"
|
||||||
class="pic-item"
|
class="pic-item"
|
||||||
>
|
>
|
||||||
<image :src="pic" mode="aspectFill" @click="previewPicList(pIdx)" />
|
<image
|
||||||
<view v-if="!reportFlag" class="del-btn" @click.stop="deletePic(pic)">
|
v-if="!reportFlag || pic.status === 'pass' || pic.status === 'unknown'"
|
||||||
|
:src="pic.url"
|
||||||
|
mode="aspectFill"
|
||||||
|
@click="previewDisplayPicList(pIdx)"
|
||||||
|
/>
|
||||||
|
<view v-else-if="pic.status === 'pending'" class="pic-audit-pending">
|
||||||
|
<text>{{ pic.statusText || '待复核' }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="pic.status === 'fail'" class="pic-audit-fail">
|
||||||
|
<text>{{ pic.statusText || '未通过审核' }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="!reportFlag" class="del-btn" @click.stop="deletePic(pic.url)">
|
||||||
<image class="del-icon" src="/static/image/svg/del.svg" mode="aspectFit" />
|
<image class="del-icon" src="/static/image/svg/del.svg" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="!reportFlag && picList.length < 3"
|
v-if="!reportFlag && displayPicList.length < 3"
|
||||||
class="add-btn"
|
class="add-btn"
|
||||||
@click="chooseImage"
|
@click="chooseImage"
|
||||||
>
|
>
|
||||||
@ -289,7 +300,10 @@
|
|||||||
<text>添加</text>
|
<text>添加</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="!reportFlag" class="upload-btn" @click="chooseImage">
|
<view v-if="reportFlag && mediaAuditIssueTip" class="audit-tip fail">
|
||||||
|
<text>{{ mediaAuditIssueTip }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!reportFlag && !displayPicList.length" class="upload-btn" @click="chooseImage">
|
||||||
<view class="upload-camera">
|
<view class="upload-camera">
|
||||||
<svg viewBox="0 0 24 24" fill="none">
|
<svg viewBox="0 0 24 24" fill="none">
|
||||||
<path d="M3 8a2 2 0 0 1 2-2h2l2-2h6l2 2h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8z"
|
<path d="M3 8a2 2 0 0 1 2-2h2l2-2h6l2 2h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8z"
|
||||||
@ -299,7 +313,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<text>拍照上传答案</text>
|
<text>拍照上传答案</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="empty-tip">
|
<view v-else-if="reportFlag && !picList.length" class="empty-tip">
|
||||||
<text>未作答</text>
|
<text>未作答</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -381,6 +395,12 @@ import { ref, computed, watch, onUnmounted } from 'vue';
|
|||||||
import { uploadFile, checkImage } from '@/api/upload';
|
import { uploadFile, checkImage } from '@/api/upload';
|
||||||
import ImageCropper from '@/components/ImageCropper/index.vue';
|
import ImageCropper from '@/components/ImageCropper/index.vue';
|
||||||
import { formatQuestionHtml } from '@/utils/questionHtml';
|
import { formatQuestionHtml } from '@/utils/questionHtml';
|
||||||
|
import {
|
||||||
|
getMediaAuditInfoForImage,
|
||||||
|
getQuestionTitleId,
|
||||||
|
type MediaAuditStatus,
|
||||||
|
} from '@/utils/mediaAudit';
|
||||||
|
import { getWxOpenid } from '@/utils/wxOpenid';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data?: any;
|
data?: any;
|
||||||
@ -795,6 +815,77 @@ const picList = computed(() => {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface PicDisplayItem {
|
||||||
|
url: string;
|
||||||
|
status: MediaAuditStatus;
|
||||||
|
labelText: string;
|
||||||
|
statusText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayPicList = computed((): PicDisplayItem[] => {
|
||||||
|
const pics = picList.value;
|
||||||
|
if (!props.reportFlag) {
|
||||||
|
return pics.map((url) => ({
|
||||||
|
url,
|
||||||
|
status: 'unknown' as MediaAuditStatus,
|
||||||
|
labelText: '',
|
||||||
|
statusText: '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auditList = props.data?.mediaAuditList;
|
||||||
|
return pics.map((url) => {
|
||||||
|
const info = getMediaAuditInfoForImage(url, auditList);
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
status: info.status,
|
||||||
|
labelText: info.labelText,
|
||||||
|
statusText: info.statusText,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const mediaAuditIssueTip = computed(() => {
|
||||||
|
if (!props.reportFlag || !picList.value.length) return '';
|
||||||
|
|
||||||
|
const issues = displayPicList.value.filter(
|
||||||
|
(item) => item.status === 'fail' || item.status === 'pending',
|
||||||
|
);
|
||||||
|
if (!issues.length) return '';
|
||||||
|
|
||||||
|
const failItems = issues.filter((item) => item.status === 'fail');
|
||||||
|
const pendingItems = issues.filter((item) => item.status === 'pending');
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (failItems.length) {
|
||||||
|
const labels = [...new Set(failItems.map((item) => item.labelText).filter(Boolean))];
|
||||||
|
const labelPart = labels.length ? `(${labels.map((t) => `涉${t}`).join('、')})` : '';
|
||||||
|
parts.push(`${failItems.length} 张图片未通过内容审核${labelPart}`);
|
||||||
|
}
|
||||||
|
if (pendingItems.length) {
|
||||||
|
const labels = [...new Set(pendingItems.map((item) => item.labelText).filter(Boolean))];
|
||||||
|
const labelPart = labels.length ? `(${labels.join('、')})` : '';
|
||||||
|
parts.push(`${pendingItems.length} 张图片待人工复核${labelPart}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(';');
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewDisplayPicList = (index: number) => {
|
||||||
|
const previewable = displayPicList.value.filter(
|
||||||
|
(item) => !props.reportFlag || item.status === 'pass' || item.status === 'unknown',
|
||||||
|
);
|
||||||
|
if (!previewable.length) return;
|
||||||
|
|
||||||
|
const current = displayPicList.value[index];
|
||||||
|
if (!current || (props.reportFlag && current.status !== 'pass' && current.status !== 'unknown')) return;
|
||||||
|
|
||||||
|
uni.previewImage({
|
||||||
|
urls: previewable.map((item) => item.url),
|
||||||
|
current: current.url,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const showCropper = ref(false);
|
const showCropper = ref(false);
|
||||||
const cropperImageSrc = ref('');
|
const cropperImageSrc = ref('');
|
||||||
|
|
||||||
@ -841,21 +932,24 @@ const uploadImage = async (filePath: string) => {
|
|||||||
emit('submit', { submitAnswerPic: submitAnswerPic.value });
|
emit('submit', { submitAnswerPic: submitAnswerPic.value });
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
|
|
||||||
// 上传成功后调用图片合规检查
|
// 上传成功后提交图片内容安全检测
|
||||||
const bizId = props.data?.id || props.data?.itemId || '';
|
const titleId = getQuestionTitleId(props.data);
|
||||||
if (bizId && newUrl) {
|
const recordId = props.recordId || '';
|
||||||
checkImage({
|
if (titleId && recordId && newUrl) {
|
||||||
bizId,
|
getWxOpenid()
|
||||||
bizType: 1,
|
.then((openid) => checkImage({
|
||||||
imageUrl: newUrl,
|
openid,
|
||||||
scene: 1,
|
recordId,
|
||||||
recordId: props.recordId,
|
titleId,
|
||||||
}).catch(() => {
|
imageUrl: newUrl,
|
||||||
// 合规检查失败不影响上传流程,静默处理
|
}))
|
||||||
});
|
.catch((err) => {
|
||||||
|
console.warn('[Question] checkImage failed', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
|
if (Number(err?.code) === 401) return;
|
||||||
uni.showToast({ title: err?.message || '上传失败', icon: 'none' });
|
uni.showToast({ title: err?.message || '上传失败', icon: 'none' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -866,10 +960,6 @@ const deletePic = (url: string) => {
|
|||||||
emit('submit', { submitAnswerPic: submitAnswerPic.value });
|
emit('submit', { submitAnswerPic: submitAnswerPic.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const previewPicList = (index: number) => {
|
|
||||||
uni.previewImage({ urls: picList.value, current: index });
|
|
||||||
};
|
|
||||||
|
|
||||||
const previewAnalyzeImage = (url: string) => {
|
const previewAnalyzeImage = (url: string) => {
|
||||||
uni.previewImage({
|
uni.previewImage({
|
||||||
urls: props.data.titleAnalyzePicture.split(','),
|
urls: props.data.titleAnalyzePicture.split(','),
|
||||||
@ -1779,6 +1869,57 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pic-audit-pending,
|
||||||
|
.pic-audit-fail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 6rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pic-audit-pending {
|
||||||
|
background: #f1f5f9;
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pic-audit-fail {
|
||||||
|
background: #fef2f2;
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-tip {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
padding: 6rpx 9rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 15rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fail {
|
||||||
|
background: #fef2f2;
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.add-btn {
|
.add-btn {
|
||||||
width: 90rpx;
|
width: 90rpx;
|
||||||
height: 90rpx;
|
height: 90rpx;
|
||||||
|
|||||||
@ -108,6 +108,9 @@ import {
|
|||||||
submitExam,
|
submitExam,
|
||||||
getPaperRecordDetail,
|
getPaperRecordDetail,
|
||||||
} from '@/api/homework';
|
} from '@/api/homework';
|
||||||
|
import { listMediaByRecordId } from '@/api/upload';
|
||||||
|
import { attachMediaAuditToPaperList } from '@/utils/mediaAudit';
|
||||||
|
import { getWxOpenid } from '@/utils/wxOpenid';
|
||||||
import {
|
import {
|
||||||
resource_type_homework,
|
resource_type_homework,
|
||||||
normalizeResourceType,
|
normalizeResourceType,
|
||||||
@ -176,7 +179,8 @@ const getSubmitAnswerPic = (item: any) => {
|
|||||||
return extractPicLinksFromAnswer(item?.submitAnswer);
|
return extractPicLinksFromAnswer(item?.submitAnswer);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getQuestionKey = (item: any) => item?.id || item?.itemId || item?.jobStuTittleId || '';
|
// 与 Web saveRecordAnswer 一致:接口 itemId 应传答卷记录题目 ID,而非题库题目 id
|
||||||
|
const getQuestionKey = (item: any) => item?.itemId || item?.jobStuTittleId || item?.id || '';
|
||||||
|
|
||||||
const normalizeCurrentMultiAnswer = () => {
|
const normalizeCurrentMultiAnswer = () => {
|
||||||
const cur = paperList.value[activeIdx.value];
|
const cur = paperList.value[activeIdx.value];
|
||||||
@ -400,7 +404,7 @@ const saveAnswer = async () => {
|
|||||||
answer: cur.submitAnswer || '',
|
answer: cur.submitAnswer || '',
|
||||||
answerPic: cur.submitAnswerPic || '',
|
answerPic: cur.submitAnswerPic || '',
|
||||||
costTime: (paperInfo.value.costTime || 0) * 1000,
|
costTime: (paperInfo.value.costTime || 0) * 1000,
|
||||||
itemId: getQuestionKey(cur),
|
itemId: cur.itemId,
|
||||||
recordId: params.value.recordId,
|
recordId: params.value.recordId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -476,18 +480,28 @@ const clearTimer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initReport = async () => {
|
const initReport = async () => {
|
||||||
|
const recordId = params.value.recordId || params.value.id;
|
||||||
const res = await getPaperRecordDetail({
|
const res = await getPaperRecordDetail({
|
||||||
recordId: params.value.recordId || params.value.id,
|
recordId,
|
||||||
reportFlag: 1,
|
reportFlag: 1,
|
||||||
});
|
});
|
||||||
paperInfo.value = res.data;
|
paperInfo.value = res.data;
|
||||||
paperList.value = normalizePaperList(res.data?.titleVoList || res.data?.subjectTitleVoList);
|
paperList.value = normalizePaperList(res.data?.titleVoList || res.data?.subjectTitleVoList);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auditRes = await listMediaByRecordId(recordId);
|
||||||
|
const auditList = Array.isArray(auditRes.data) ? auditRes.data : [];
|
||||||
|
attachMediaAuditToPaperList(paperList.value, auditList);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[doWork] media audit query failed', err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initNewTrain = async () => {
|
const initNewTrain = async () => {
|
||||||
if (resourceType.value !== resource_type_homework) {
|
if (resourceType.value !== resource_type_homework) {
|
||||||
throw new Error('暂不支持该练习类型');
|
throw new Error('暂不支持该练习类型');
|
||||||
}
|
}
|
||||||
|
await getWxOpenid();
|
||||||
const res: any = await startExam(params.value.recordId);
|
const res: any = await startExam(params.value.recordId);
|
||||||
paperInfo.value = res.data || res;
|
paperInfo.value = res.data || res;
|
||||||
if (paperInfo.value.costTime) {
|
if (paperInfo.value.costTime) {
|
||||||
@ -537,6 +551,8 @@ const init = async () => {
|
|||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[doWork] init err', err);
|
console.error('[doWork] init err', err);
|
||||||
|
// 401 已由 request 拦截器统一跳转登录,避免 navigateBack 覆盖 reLaunch
|
||||||
|
if (Number(err?.code) === 401) return;
|
||||||
uni.showToast({ title: err?.message || '加载失败', icon: 'none' });
|
uni.showToast({ title: err?.message || '加载失败', icon: 'none' });
|
||||||
setTimeout(() => uni.navigateBack(), 1500);
|
setTimeout(() => uni.navigateBack(), 1500);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -151,6 +151,7 @@ import Loading from '@/components/Loading/index.vue';
|
|||||||
import { getUnfinishedHomework, type PaperRecord } from '@/api/homework';
|
import { getUnfinishedHomework, type PaperRecord } from '@/api/homework';
|
||||||
import { resource_type_homework } from '@/constants/doWork';
|
import { resource_type_homework } from '@/constants/doWork';
|
||||||
import { formatTime } from '@/utils/format';
|
import { formatTime } from '@/utils/format';
|
||||||
|
import { getWxOpenid } from '@/utils/wxOpenid';
|
||||||
|
|
||||||
interface SubjectItem {
|
interface SubjectItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -159,6 +160,7 @@ interface SubjectItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pageLoading = ref(false);
|
const pageLoading = ref(false);
|
||||||
|
const startingPaper = ref(false);
|
||||||
const subjectList = ref<SubjectItem[]>([]);
|
const subjectList = ref<SubjectItem[]>([]);
|
||||||
const subjectPaperMap = ref(new Map<string, PaperRecord[]>());
|
const subjectPaperMap = ref(new Map<string, PaperRecord[]>());
|
||||||
const activeSubjectId = ref('');
|
const activeSubjectId = ref('');
|
||||||
@ -219,10 +221,21 @@ const onSelectSubject = (item: SubjectItem) => {
|
|||||||
activeSubjectId.value = item.id;
|
activeSubjectId.value = item.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onStartPaper = (paper: PaperRecord) => {
|
const onStartPaper = async (paper: PaperRecord) => {
|
||||||
uni.navigateTo({
|
if (startingPaper.value) return;
|
||||||
url: `/pages/doWork/index?resourceType=${resource_type_homework}&recordId=${paper.id}&reportFlag=0&subjectId=${activeSubjectId.value}`,
|
startingPaper.value = true;
|
||||||
});
|
uni.showLoading({ title: '准备中...', mask: true });
|
||||||
|
try {
|
||||||
|
await getWxOpenid();
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/doWork/index?resourceType=${resource_type_homework}&recordId=${paper.id}&reportFlag=0&subjectId=${activeSubjectId.value}`,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
uni.showToast({ title: err?.message || '获取微信授权失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading();
|
||||||
|
startingPaper.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initData = async () => {
|
const initData = async () => {
|
||||||
|
|||||||
@ -184,6 +184,7 @@ import { parentMpLogin, sendParentLoginCode } from '@/api/login';
|
|||||||
import { useUserStore } from '@/state/modules/user';
|
import { useUserStore } from '@/state/modules/user';
|
||||||
import { useTeacherStore } from '@/state/modules/teacher';
|
import { useTeacherStore } from '@/state/modules/teacher';
|
||||||
import { setCache, getCache } from '@/utils/cache';
|
import { setCache, getCache } from '@/utils/cache';
|
||||||
|
import { cacheWxOpenid } from '@/utils/wxOpenid';
|
||||||
import { redirectToRoleHomeIfNeeded } from '@/utils/roleHome';
|
import { redirectToRoleHomeIfNeeded } from '@/utils/roleHome';
|
||||||
|
|
||||||
const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com';
|
const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com';
|
||||||
@ -368,6 +369,7 @@ const onParentAuthorize = async () => {
|
|||||||
const res = await parentMpLogin(code);
|
const res = await parentMpLogin(code);
|
||||||
const data = res?.data || {};
|
const data = res?.data || {};
|
||||||
const accessToken = data.accessToken;
|
const accessToken = data.accessToken;
|
||||||
|
cacheWxOpenid(data.wxUser?.openid);
|
||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
await userStore.parentMpLoginSuccess(accessToken);
|
await userStore.parentMpLoginSuccess(accessToken);
|
||||||
@ -385,6 +387,7 @@ const onParentAuthorize = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
parentWxOpenid.value = openid;
|
parentWxOpenid.value = openid;
|
||||||
|
cacheWxOpenid(openid);
|
||||||
parentStage.value = 'bind';
|
parentStage.value = 'bind';
|
||||||
if (data.phone) {
|
if (data.phone) {
|
||||||
phone.value = data.phone;
|
phone.value = data.phone;
|
||||||
|
|||||||
@ -48,7 +48,8 @@ export function userRouternext(router: Router) {
|
|||||||
|
|
||||||
router.afterEach((to) => {
|
router.afterEach((to) => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
if (userStore.isLogin && to?.name === 'login') {
|
// 401 清缓存后 Pinia 可能仍短暂为已登录,需同时校验本地 token
|
||||||
|
if (userStore.isLogin && getCache('token') && to?.name === 'login') {
|
||||||
const role = getStoredRole();
|
const role = getStoredRole();
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
const homeName = role === 'teacher' ? 'teacher-home' : role === 'parent' ? 'parent-home' : 'home';
|
const homeName = role === 'teacher' ? 'teacher-home' : role === 'parent' ? 'parent-home' : 'home';
|
||||||
|
|||||||
239
src/utils/mediaAudit.ts
Normal file
239
src/utils/mediaAudit.ts
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
import type { MediaAuditRecord } from '@/api/upload';
|
||||||
|
|
||||||
|
export type MediaAuditStatus = 'pass' | 'fail' | 'pending' | 'unknown';
|
||||||
|
|
||||||
|
/** 微信内容安全 label 枚举 */
|
||||||
|
export const MEDIA_AUDIT_LABEL_TEXT: Record<number, string> = {
|
||||||
|
100: '正常',
|
||||||
|
20001: '时政',
|
||||||
|
20002: '色情',
|
||||||
|
20006: '违法犯罪',
|
||||||
|
21000: '其他',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MediaAuditDisplayInfo {
|
||||||
|
status: MediaAuditStatus;
|
||||||
|
label: number;
|
||||||
|
labelText: string;
|
||||||
|
statusText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMediaAuditLabelText = (label?: number | string | null) => {
|
||||||
|
const num = Number(label);
|
||||||
|
if (!Number.isFinite(num) || num === 100) return '';
|
||||||
|
return MEDIA_AUDIT_LABEL_TEXT[num] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMediaAuditStatusText = (
|
||||||
|
status: MediaAuditStatus,
|
||||||
|
label?: number | string | null,
|
||||||
|
) => {
|
||||||
|
const labelText = getMediaAuditLabelText(label);
|
||||||
|
if (status === 'pass' || status === 'unknown') return '';
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return labelText ? `${labelText}·待复核` : '待复核';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'fail') {
|
||||||
|
return labelText ? `涉${labelText}内容` : '未通过审核';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeImageUrl = (url?: string) =>
|
||||||
|
String(url || '')
|
||||||
|
.replace(/[`]/g, '')
|
||||||
|
.replace(/http:\/\//gi, 'https://')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
/** 取 URL 文件名作为弱匹配键,避免域名或协议差异导致匹配失败 */
|
||||||
|
const getImageUrlFileKey = (url?: string) => {
|
||||||
|
const normalized = normalizeImageUrl(url);
|
||||||
|
if (!normalized) return '';
|
||||||
|
const withoutQuery = normalized.split('?')[0].split('#')[0];
|
||||||
|
const segments = withoutQuery.split('/').filter(Boolean);
|
||||||
|
return segments[segments.length - 1] || withoutQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMediaAuditImageUrl = (item: MediaAuditRecord) =>
|
||||||
|
normalizeImageUrl(item.imageUrl || item.mediaUrl || item.url);
|
||||||
|
|
||||||
|
/** 题目侧可用于与审核记录 titleId / bizId 对齐的 ID */
|
||||||
|
export const getQuestionMatchKeys = (item: any) =>
|
||||||
|
[
|
||||||
|
item?.jobStuTittleId,
|
||||||
|
item?.id,
|
||||||
|
item?.titleId,
|
||||||
|
item?.questionId,
|
||||||
|
item?.titleInfoId,
|
||||||
|
item?.itemId,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(String);
|
||||||
|
|
||||||
|
/** 提交图片审核时使用的题目 ID,优先与接口 titleId 字段一致 */
|
||||||
|
export const getQuestionTitleId = (item: any) => {
|
||||||
|
const keys = getQuestionMatchKeys(item);
|
||||||
|
return keys[0] || keys[1] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 审核记录侧可用于分组的 ID */
|
||||||
|
export const getAuditRecordMatchKeys = (item: MediaAuditRecord) =>
|
||||||
|
[item.titleId, item.bizId].filter(Boolean).map(String);
|
||||||
|
|
||||||
|
export const normalizeMediaAuditStatus = (item: MediaAuditRecord): MediaAuditStatus => {
|
||||||
|
const suggest = String(item.suggest || '').toLowerCase();
|
||||||
|
if (suggest === 'pass') return 'pass';
|
||||||
|
if (suggest === 'risky') return 'fail';
|
||||||
|
if (suggest === 'review') return 'pending';
|
||||||
|
|
||||||
|
if (item.pass === true || item.auditPass === true) return 'pass';
|
||||||
|
if (item.pass === false || item.auditPass === false) return 'fail';
|
||||||
|
|
||||||
|
const rawStatus = item.status ?? item.auditStatus ?? item.checkStatus ?? item.result;
|
||||||
|
const statusText = String(rawStatus ?? '').toLowerCase();
|
||||||
|
|
||||||
|
if (rawStatus === true || statusText === '1' || statusText === 'pass') return 'pass';
|
||||||
|
if (rawStatus === false || statusText === '2' || statusText === 'fail' || statusText === 'risky') {
|
||||||
|
return 'fail';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rawStatus === 0
|
||||||
|
|| statusText === '0'
|
||||||
|
|| statusText === 'pending'
|
||||||
|
|| statusText === 'review'
|
||||||
|
) {
|
||||||
|
return 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.label != null && item.label !== 100) {
|
||||||
|
if (suggest === 'pass') return 'pass';
|
||||||
|
if (suggest === 'review') return 'pending';
|
||||||
|
if (suggest === 'risky') return 'fail';
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = String(item.auditResult || item.resultMsg || item.message || '').toLowerCase();
|
||||||
|
if (/pass|success|合规|通过/.test(text)) return 'pass';
|
||||||
|
if (/fail|reject|risky|违规|不合规|不通过/.test(text)) return 'fail';
|
||||||
|
if (/pending|review|审核中|复核|处理中/.test(text)) return 'pending';
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
const dedupeAuditRecords = (records: MediaAuditRecord[]) => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return records.filter((item) => {
|
||||||
|
const key = [
|
||||||
|
String(item.id || ''),
|
||||||
|
String(item.titleId || item.bizId || ''),
|
||||||
|
getMediaAuditImageUrl(item),
|
||||||
|
String(item.status ?? item.auditStatus ?? item.suggest ?? ''),
|
||||||
|
].join('|');
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return Boolean(getMediaAuditImageUrl(item) || item.titleId || item.bizId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const groupMediaAuditByTitleId = (records: MediaAuditRecord[]) => {
|
||||||
|
const grouped = new Map<string, MediaAuditRecord[]>();
|
||||||
|
records.forEach((item) => {
|
||||||
|
getAuditRecordMatchKeys(item).forEach((key) => {
|
||||||
|
const list = grouped.get(key) || [];
|
||||||
|
list.push(item);
|
||||||
|
grouped.set(key, list);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return grouped;
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupMediaAuditByImageFile = (records: MediaAuditRecord[]) => {
|
||||||
|
const grouped = new Map<string, MediaAuditRecord[]>();
|
||||||
|
records.forEach((item) => {
|
||||||
|
const fileKey = getImageUrlFileKey(getMediaAuditImageUrl(item));
|
||||||
|
if (!fileKey) return;
|
||||||
|
const list = grouped.get(fileKey) || [];
|
||||||
|
list.push(item);
|
||||||
|
grouped.set(fileKey, list);
|
||||||
|
});
|
||||||
|
return grouped;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQuestionImageUrls = (item: any) =>
|
||||||
|
String(item?.submitAnswerPic || '')
|
||||||
|
.split(',')
|
||||||
|
.map((url) => normalizeImageUrl(url))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 recordId 下全部审核记录,按题目 ID 与图片 URL 分发到各题
|
||||||
|
*/
|
||||||
|
export const attachMediaAuditToPaperList = (
|
||||||
|
paperList: any[],
|
||||||
|
auditRecords: MediaAuditRecord[] | null | undefined,
|
||||||
|
) => {
|
||||||
|
if (!Array.isArray(paperList) || !Array.isArray(auditRecords)) return;
|
||||||
|
|
||||||
|
const groupedByTitle = groupMediaAuditByTitleId(auditRecords);
|
||||||
|
const groupedByFile = groupMediaAuditByImageFile(auditRecords);
|
||||||
|
|
||||||
|
paperList.forEach((item) => {
|
||||||
|
const matched: MediaAuditRecord[] = [];
|
||||||
|
|
||||||
|
getQuestionMatchKeys(item).forEach((key) => {
|
||||||
|
matched.push(...(groupedByTitle.get(key) || []));
|
||||||
|
});
|
||||||
|
|
||||||
|
getQuestionImageUrls(item).forEach((url) => {
|
||||||
|
const fileKey = getImageUrlFileKey(url);
|
||||||
|
if (fileKey) {
|
||||||
|
matched.push(...(groupedByFile.get(fileKey) || []));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
item.mediaAuditList = dedupeAuditRecords(matched);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findMediaAuditForImage = (
|
||||||
|
imageUrl: string,
|
||||||
|
auditList: MediaAuditRecord[] | undefined,
|
||||||
|
) => {
|
||||||
|
if (!auditList?.length) return undefined;
|
||||||
|
|
||||||
|
const target = normalizeImageUrl(imageUrl);
|
||||||
|
const exact = auditList.find((item) => getMediaAuditImageUrl(item) === target);
|
||||||
|
if (exact) return exact;
|
||||||
|
|
||||||
|
const fileKey = getImageUrlFileKey(target);
|
||||||
|
if (!fileKey) return undefined;
|
||||||
|
|
||||||
|
return auditList.find((item) => getImageUrlFileKey(getMediaAuditImageUrl(item)) === fileKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMediaAuditStatusForImage = (
|
||||||
|
imageUrl: string,
|
||||||
|
auditList: MediaAuditRecord[] | undefined,
|
||||||
|
): MediaAuditStatus => {
|
||||||
|
const matched = findMediaAuditForImage(imageUrl, auditList);
|
||||||
|
return matched ? normalizeMediaAuditStatus(matched) : 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMediaAuditInfoForImage = (
|
||||||
|
imageUrl: string,
|
||||||
|
auditList: MediaAuditRecord[] | undefined,
|
||||||
|
): MediaAuditDisplayInfo => {
|
||||||
|
const matched = findMediaAuditForImage(imageUrl, auditList);
|
||||||
|
const status = matched ? normalizeMediaAuditStatus(matched) : 'unknown';
|
||||||
|
const label = Number(matched?.label ?? 100);
|
||||||
|
const labelText = getMediaAuditLabelText(label);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
label,
|
||||||
|
labelText,
|
||||||
|
statusText: getMediaAuditStatusText(status, label),
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import Request from 'luch-request';
|
import Request from 'luch-request';
|
||||||
import { getCache, removeCache } from '@/utils/cache';
|
import { getCache } from '@/utils/cache';
|
||||||
|
import { clearSession } from '@/utils/session';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 灵犀学 AI 通用 HTTP 客户端(基于 luch-request)
|
* 灵犀学 AI 通用 HTTP 客户端(基于 luch-request)
|
||||||
@ -43,18 +44,47 @@ const http = new Request({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleUnauthorized = (message = '登录已失效,请重新登录') => {
|
const LOGIN_URL = '/pages/login/index';
|
||||||
|
|
||||||
|
const goLoginPage = () => {
|
||||||
|
uni.reLaunch({
|
||||||
|
url: LOGIN_URL,
|
||||||
|
fail: () => {
|
||||||
|
uni.redirectTo({ url: LOGIN_URL });
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
isReloginHandling = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleUnauthorized = (message = '登录已失效,请重新登录') => {
|
||||||
if (isReloginHandling) return;
|
if (isReloginHandling) return;
|
||||||
isReloginHandling = true;
|
isReloginHandling = true;
|
||||||
removeCache('token');
|
|
||||||
removeCache('userInfo');
|
clearSession();
|
||||||
removeCache('teacherInfo');
|
|
||||||
removeCache('role');
|
// 小程序中 showToast 期间 navigate 可能被拦截,先跳转再提示
|
||||||
uni.showToast({ title: message, icon: 'none', duration: 2500 });
|
goLoginPage();
|
||||||
// 避免并发 401 触发多次跳转与多次弹窗
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.reLaunch({ url: '/pages/login/index' });
|
uni.showToast({ title: message, icon: 'none', duration: 2500 });
|
||||||
}, 200);
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUnauthorizedCode = (code: unknown) => Number(code) === 401;
|
||||||
|
|
||||||
|
const normalizeResponseData = (raw: unknown): BackendResp | null => {
|
||||||
|
if (!raw) return null;
|
||||||
|
if (typeof raw === 'object') return raw as BackendResp;
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return parsed && typeof parsed === 'object' ? (parsed as BackendResp) : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------- 请求拦截 -----------
|
// ----------- 请求拦截 -----------
|
||||||
@ -79,8 +109,8 @@ http.interceptors.request.use(
|
|||||||
// ----------- 响应拦截 -----------
|
// ----------- 响应拦截 -----------
|
||||||
http.interceptors.response.use(
|
http.interceptors.response.use(
|
||||||
(response: any) => {
|
(response: any) => {
|
||||||
const data = response.data as BackendResp;
|
const data = normalizeResponseData(response.data);
|
||||||
if (!data || typeof data !== 'object') {
|
if (!data) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +119,7 @@ http.interceptors.response.use(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.code === 401) {
|
if (isUnauthorizedCode(data.code)) {
|
||||||
const msg401 = data.message || data.msg || '登录已失效,请重新登录';
|
const msg401 = data.message || data.msg || '登录已失效,请重新登录';
|
||||||
handleUnauthorized(msg401);
|
handleUnauthorized(msg401);
|
||||||
return Promise.reject(new HttpError(msg401, 401, data));
|
return Promise.reject(new HttpError(msg401, 401, data));
|
||||||
@ -103,19 +133,21 @@ http.interceptors.response.use(
|
|||||||
return Promise.reject(new HttpError(msg, data.code, data));
|
return Promise.reject(new HttpError(msg, data.code, data));
|
||||||
},
|
},
|
||||||
(err: any) => {
|
(err: any) => {
|
||||||
|
const body = normalizeResponseData(err?.data);
|
||||||
const status = err?.statusCode || err?.status;
|
const status = err?.statusCode || err?.status;
|
||||||
let msg = err?.errMsg || err?.message || '网络异常';
|
let msg = body?.message || body?.msg || err?.errMsg || err?.message || '网络异常';
|
||||||
|
|
||||||
if (status === 401) {
|
if (isUnauthorizedCode(status) || isUnauthorizedCode(body?.code)) {
|
||||||
msg = '登录已失效,请重新登录';
|
msg = body?.message || body?.msg || '登录已失效,请重新登录';
|
||||||
handleUnauthorized(msg);
|
handleUnauthorized(msg);
|
||||||
} else if (status === 500) {
|
return Promise.reject(new HttpError(msg, 401, body || err));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 500) {
|
||||||
msg = '服务异常,请稍后重试';
|
msg = '服务异常,请稍后重试';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status !== 401) {
|
uni.showToast({ title: msg, icon: 'none', duration: 2500 });
|
||||||
uni.showToast({ title: msg, icon: 'none', duration: 2500 });
|
|
||||||
}
|
|
||||||
return Promise.reject(new HttpError(msg, status || -1, err));
|
return Promise.reject(new HttpError(msg, status || -1, err));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
10
src/utils/session.ts
Normal file
10
src/utils/session.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/** 401 等场景统一清空登录态(由 main.ts 注册 Pinia 同步逻辑) */
|
||||||
|
let clearSessionHandler: (() => void) | null = null;
|
||||||
|
|
||||||
|
export const registerClearSessionHandler = (handler: () => void) => {
|
||||||
|
clearSessionHandler = handler;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearSession = () => {
|
||||||
|
clearSessionHandler?.();
|
||||||
|
};
|
||||||
58
src/utils/wxOpenid.ts
Normal file
58
src/utils/wxOpenid.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { parentMpLogin } from '@/api/login';
|
||||||
|
import { getCache, setCache } from '@/utils/cache';
|
||||||
|
|
||||||
|
let openidPromise: Promise<string> | null = null;
|
||||||
|
|
||||||
|
const readCachedOpenid = () => {
|
||||||
|
const cachedOpenid = getCache('wxOpenid');
|
||||||
|
if (cachedOpenid) return String(cachedOpenid);
|
||||||
|
|
||||||
|
const userInfo = getCache('userInfo');
|
||||||
|
if (userInfo && typeof userInfo === 'object') {
|
||||||
|
const openid = userInfo.openid || userInfo.openId || userInfo.wxOpenid;
|
||||||
|
if (openid) return String(openid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取当前微信 openid,优先读缓存,否则通过 uni.login + mpLogin 换取并持久化 */
|
||||||
|
export const getWxOpenid = async (): Promise<string> => {
|
||||||
|
const cached = readCachedOpenid();
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
if (!openidPromise) {
|
||||||
|
openidPromise = (async () => {
|
||||||
|
const loginRes = await new Promise<UniApp.LoginRes>((resolve, reject) => {
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: resolve,
|
||||||
|
fail: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = loginRes?.code || '';
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('获取微信 code 失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await parentMpLogin(code);
|
||||||
|
const openid = res?.data?.wxUser?.openid || '';
|
||||||
|
if (!openid) {
|
||||||
|
throw new Error('获取微信 openid 失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
setCache('wxOpenid', openid);
|
||||||
|
return openid;
|
||||||
|
})().finally(() => {
|
||||||
|
openidPromise = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return openidPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cacheWxOpenid = (openid?: string) => {
|
||||||
|
if (!openid) return;
|
||||||
|
setCache('wxOpenid', openid);
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user