微信登录日志
This commit is contained in:
@@ -7,6 +7,39 @@ export type ChooseWechatImageOptions = {
|
|||||||
sourceType?: Array<'album' | 'camera'>;
|
sourceType?: Array<'album' | 'camera'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function reportChooseImageEvent(
|
||||||
|
config: WeixinSdkConfig,
|
||||||
|
payload: {
|
||||||
|
status: 'fail';
|
||||||
|
errMsg: string;
|
||||||
|
sourceType?: string;
|
||||||
|
stage: 'jssdk' | 'choose' | 'read' | 'empty';
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (/cancel/i.test(payload.errMsg)) return;
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Client-App': config.clientApp,
|
||||||
|
};
|
||||||
|
const token = config.getAccessToken?.();
|
||||||
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
|
await fetch(`${config.apiBase ?? '/api/v1'}/common/wechat/choose-image`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: 'fail',
|
||||||
|
errMsg: payload.errMsg,
|
||||||
|
sourceType: payload.sourceType,
|
||||||
|
stage: payload.stage,
|
||||||
|
pageUrl: typeof window !== 'undefined' ? window.location.href.split('#')[0] : undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* 上报失败不影响主流程 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function base64ToFile(base64: string, fileName: string): File {
|
function base64ToFile(base64: string, fileName: string): File {
|
||||||
const normalized = base64.startsWith('data:')
|
const normalized = base64.startsWith('data:')
|
||||||
? base64
|
? base64
|
||||||
@@ -52,30 +85,58 @@ export async function chooseWechatImages(
|
|||||||
|
|
||||||
if (platform === 'wechat-h5') {
|
if (platform === 'wechat-h5') {
|
||||||
const pageUrl = typeof window !== 'undefined' ? window.location.href.split('#')[0] : '';
|
const pageUrl = typeof window !== 'undefined' ? window.location.href.split('#')[0] : '';
|
||||||
await ensureJssdkReady({
|
const sourceTypeKey = sourceType.join(',');
|
||||||
apiBase: config.apiBase ?? '/api/v1',
|
try {
|
||||||
clientApp: config.clientApp,
|
await ensureJssdkReady({
|
||||||
getAccessToken: config.getAccessToken,
|
apiBase: config.apiBase ?? '/api/v1',
|
||||||
url: pageUrl,
|
clientApp: config.clientApp,
|
||||||
jsApiList: ['chooseImage', 'getLocalImgData'],
|
getAccessToken: config.getAccessToken,
|
||||||
});
|
url: pageUrl,
|
||||||
|
jsApiList: ['chooseImage', 'getLocalImgData'],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const errMsg = e instanceof Error ? e.message : 'JSSDK 初始化失败';
|
||||||
|
void reportChooseImageEvent(config, { status: 'fail', errMsg, sourceType: sourceTypeKey, stage: 'jssdk' });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
if (!window.wx?.chooseImage) {
|
if (!window.wx?.chooseImage) {
|
||||||
throw new Error('微信选图接口不可用,请刷新页面后重试');
|
const errMsg = '微信选图接口不可用,请刷新页面后重试';
|
||||||
|
void reportChooseImageEvent(config, { status: 'fail', errMsg, sourceType: sourceTypeKey, stage: 'jssdk' });
|
||||||
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const localIds = await new Promise<string[]>((resolve, reject) => {
|
let localIds: string[];
|
||||||
window.wx!.chooseImage!({
|
try {
|
||||||
count,
|
localIds = await new Promise<string[]>((resolve, reject) => {
|
||||||
sizeType: ['compressed'],
|
window.wx!.chooseImage!({
|
||||||
sourceType,
|
count,
|
||||||
success: (res) => resolve(res.localIds ?? []),
|
sizeType: ['compressed'],
|
||||||
fail: (err) => reject(new Error(err.errMsg || '无法打开相册')),
|
sourceType,
|
||||||
|
success: (res) => resolve(res.localIds ?? []),
|
||||||
|
fail: (err) => reject(new Error(err.errMsg || '无法打开相册')),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
} catch (e) {
|
||||||
|
const errMsg = e instanceof Error ? e.message : '无法打开相册';
|
||||||
|
void reportChooseImageEvent(config, { status: 'fail', errMsg, sourceType: sourceTypeKey, stage: 'choose' });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localIds.length) {
|
||||||
|
const errMsg = '未选择图片';
|
||||||
|
void reportChooseImageEvent(config, { status: 'fail', errMsg, sourceType: sourceTypeKey, stage: 'empty' });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const files: File[] = [];
|
const files: File[] = [];
|
||||||
for (const localId of localIds) {
|
for (const localId of localIds) {
|
||||||
files.push(await localIdToFile(localId));
|
try {
|
||||||
|
files.push(await localIdToFile(localId));
|
||||||
|
} catch (e) {
|
||||||
|
const errMsg = e instanceof Error ? e.message : '读取图片失败';
|
||||||
|
void reportChooseImageEvent(config, { status: 'fail', errMsg, sourceType: sourceTypeKey, stage: 'read' });
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ export type WechatActorRef = {
|
|||||||
refId: bigint;
|
refId: bigint;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function wechatActorRefFromAuth(actorType?: string, actorId?: bigint): WechatActorRef | undefined {
|
||||||
|
if (!actorType || actorId == null) return undefined;
|
||||||
|
return { refType: actorType, refId: actorId };
|
||||||
|
}
|
||||||
|
|
||||||
type LogWechatAuthInput = {
|
type LogWechatAuthInput = {
|
||||||
scene: string;
|
scene: string;
|
||||||
requestUrl?: string;
|
requestUrl?: string;
|
||||||
|
|||||||
@@ -128,4 +128,29 @@ export class WechatLocationService {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reportChooseImage(input: {
|
||||||
|
status: 'success' | 'fail';
|
||||||
|
errMsg?: string;
|
||||||
|
sourceType?: string;
|
||||||
|
stage?: string;
|
||||||
|
pageUrl?: string;
|
||||||
|
actorRef?: WechatActorRef;
|
||||||
|
}) {
|
||||||
|
const logId = await logWechatAuth(this.prisma, {
|
||||||
|
scene: 'CHOOSE_IMAGE',
|
||||||
|
requestUrl: input.pageUrl?.split('#')[0]?.slice(0, 512),
|
||||||
|
requestBody: {
|
||||||
|
status: input.status,
|
||||||
|
...(input.sourceType ? { sourceType: input.sourceType } : {}),
|
||||||
|
...(input.stage ? { stage: input.stage } : {}),
|
||||||
|
...(input.errMsg ? { errMsg: input.errMsg.slice(0, 200) } : {}),
|
||||||
|
},
|
||||||
|
responseBody: { reported: true },
|
||||||
|
status: input.status === 'success' ? 'SUCCESS' : 'FAILED',
|
||||||
|
errorMessage: input.status === 'fail' ? input.errMsg?.slice(0, 512) : undefined,
|
||||||
|
actorRef: input.actorRef,
|
||||||
|
});
|
||||||
|
return { ok: true, logId: logId.toString() };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { WECHAT_PROVIDER } from '../../integrations/integrations.constants';
|
|||||||
import type { IWechatProvider } from '../../integrations/wechat/wechat.interface';
|
import type { IWechatProvider } from '../../integrations/wechat/wechat.interface';
|
||||||
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
|
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
|
||||||
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { wechatActorRefFromAuth } from '../../integrations/wechat/wechat-log.util';
|
||||||
import { WechatLocationService } from './wechat-location.service';
|
import { WechatLocationService } from './wechat-location.service';
|
||||||
|
|
||||||
class PhoneNumberDto {
|
class PhoneNumberDto {
|
||||||
@@ -41,6 +42,29 @@ class WechatLocationDto {
|
|||||||
errMsg?: string;
|
errMsg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WechatChooseImageDto {
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['success', 'fail'])
|
||||||
|
status: 'success' | 'fail';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
errMsg?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
sourceType?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['jssdk', 'choose', 'read', 'empty'])
|
||||||
|
@IsOptional()
|
||||||
|
stage?: 'jssdk' | 'choose' | 'read' | 'empty';
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
pageUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Controller('common/wechat')
|
@Controller('common/wechat')
|
||||||
export class WechatController {
|
export class WechatController {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -91,4 +115,18 @@ export class WechatController {
|
|||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('choose-image')
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
|
reportChooseImage(@Req() req: Request, @Body() dto: WechatChooseImageDto) {
|
||||||
|
const user = (req as Request & { user?: AuthUser }).user;
|
||||||
|
return this.locationService.reportChooseImage({
|
||||||
|
status: dto.status,
|
||||||
|
errMsg: dto.errMsg,
|
||||||
|
sourceType: dto.sourceType,
|
||||||
|
stage: dto.stage,
|
||||||
|
pageUrl: dto.pageUrl,
|
||||||
|
actorRef: wechatActorRefFromAuth(user?.actorType, user?.actorId),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user