diff --git a/apps/admin-web/src/admin.css b/apps/admin-web/src/admin.css
index 42e62cd..cd49eea 100644
--- a/apps/admin-web/src/admin.css
+++ b/apps/admin-web/src/admin.css
@@ -15,10 +15,74 @@ body,
display: none;
}
-.admin-table-nowrap .ant-table-cell {
+/*
+ * 后台内容行:不换行 + 超出省略
+ * 覆盖主内容区、Drawer、Modal 内的表格与描述列表
+ */
+.admin-layout .ant-table-cell,
+.ant-drawer .ant-table-cell,
+.ant-modal .ant-table-cell,
+.admin-layout .ant-descriptions-item-content,
+.ant-drawer .ant-descriptions-item-content,
+.ant-modal .ant-descriptions-item-content {
white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 0;
}
+.admin-layout .ant-descriptions-item-content,
+.ant-drawer .ant-descriptions-item-content,
+.ant-modal .ant-descriptions-item-content {
+ max-width: 100%;
+}
+
+/* 表头同样不换行 */
+.admin-layout .ant-table-thead > tr > th,
+.ant-drawer .ant-table-thead > tr > th,
+.ant-modal .ant-table-thead > tr > th {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* 操作列、含交互控件的不截断 */
+.admin-layout .ant-table-cell:has(.ant-btn),
+.ant-drawer .ant-table-cell:has(.ant-btn),
+.ant-modal .ant-table-cell:has(.ant-btn),
+.admin-layout .ant-table-cell:has(.ant-space),
+.ant-drawer .ant-table-cell:has(.ant-space),
+.ant-modal .ant-table-cell:has(.ant-space) {
+ overflow: visible;
+ text-overflow: clip;
+ max-width: none;
+}
+
+/* 代码块、表单区域保持原样 */
+.admin-layout pre,
+.ant-drawer pre,
+.ant-modal pre,
+.admin-layout .ant-form-item-control-input,
+.ant-drawer .ant-form-item-control-input,
+.ant-modal .ant-form-item-control-input {
+ white-space: pre-wrap;
+ overflow: auto;
+ text-overflow: unset;
+ max-width: none;
+}
+
+/* 表格单元格双行信息合并为单行省略 */
+.admin-cell-line {
+ display: block;
+ max-width: 100%;
+}
+
+.admin-cell-line-secondary {
+ color: rgba(0, 0, 0, 0.45);
+ font-size: 12px;
+}
+
+.admin-table-nowrap .ant-table-cell,
.admin-table-nowrap .ant-table-cell-ellipsis {
white-space: nowrap;
}
diff --git a/apps/admin-web/src/components/AdminCellLine.tsx b/apps/admin-web/src/components/AdminCellLine.tsx
new file mode 100644
index 0000000..ab210df
--- /dev/null
+++ b/apps/admin-web/src/components/AdminCellLine.tsx
@@ -0,0 +1,34 @@
+import { Typography } from 'antd';
+
+type AdminCellLineProps = {
+ primary?: string | null;
+ secondary?: string | null;
+ separator?: string;
+};
+
+/** 表格单元格:主/副信息同一行展示,超出省略,hover 显示全文 */
+export function AdminCellLine({
+ primary,
+ secondary,
+ separator = ' · ',
+}: AdminCellLineProps) {
+ const main = primary?.trim() || '—';
+ const sub = secondary?.trim();
+ const full = sub ? `${main}${separator}${sub}` : main;
+
+ return (
+
+ {main}
+ {sub ? (
+
+ {separator}
+ {sub}
+
+ ) : null}
+
+ );
+}
diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx
index 5a71690..139d7b1 100644
--- a/apps/admin-web/src/layouts/AdminLayout.tsx
+++ b/apps/admin-web/src/layouts/AdminLayout.tsx
@@ -16,6 +16,7 @@ import {
FileTextOutlined,
} from '@ant-design/icons';
import { clearAuth, request, type HqProfile } from '../lib/api';
+import { bindAdminEllipsisTitle } from '../lib/ellipsis-title';
const { Header, Sider, Content } = Layout;
@@ -105,6 +106,10 @@ export default function AdminLayout() {
contentRef.current?.scrollTo({ top: 0, left: 0 });
}, [location.pathname]);
+ useEffect(() => {
+ return bindAdminEllipsisTitle();
+ }, []);
+
function logout() {
clearAuth();
navigate('/login');
@@ -164,6 +169,7 @@ export default function AdminLayout() {
diff --git a/apps/admin-web/src/lib/ellipsis-title.ts b/apps/admin-web/src/lib/ellipsis-title.ts
new file mode 100644
index 0000000..82d6045
--- /dev/null
+++ b/apps/admin-web/src/lib/ellipsis-title.ts
@@ -0,0 +1,37 @@
+const ELLIPSIS_TARGETS =
+ '.ant-table-cell, .ant-descriptions-item-content, .ant-typography';
+
+const ELLIPSIS_SKIP =
+ '.ant-btn, .ant-input, .ant-select, .ant-picker, .ant-checkbox, .ant-radio, pre, textarea, input, select, button, a.ant-btn';
+
+function isTruncated(el: HTMLElement): boolean {
+ return el.scrollWidth > el.clientWidth + 1;
+}
+
+function resolveEllipsisElement(target: EventTarget | null): HTMLElement | null {
+ if (!(target instanceof HTMLElement)) return null;
+ const cell = target.closest(ELLIPSIS_TARGETS);
+ if (!cell) return null;
+ if (cell.closest('.admin-cell-line')) return null;
+ if (cell.closest(ELLIPSIS_SKIP)) return null;
+ if (cell.querySelector(ELLIPSIS_SKIP)) return null;
+ return cell;
+}
+
+export function bindAdminEllipsisTitle(root: HTMLElement | Document = document) {
+ const onMouseOver = (event: MouseEvent) => {
+ const el = resolveEllipsisElement(event.target);
+ if (!el) return;
+ if (isTruncated(el)) {
+ const text = el.innerText.replace(/\s+/g, ' ').trim();
+ if (text && el.getAttribute('title') !== text) {
+ el.setAttribute('title', text);
+ }
+ } else {
+ el.removeAttribute('title');
+ }
+ };
+
+ root.addEventListener('mouseover', onMouseOver, true);
+ return () => root.removeEventListener('mouseover', onMouseOver, true);
+}
diff --git a/apps/admin-web/src/pages/HqLogsPage.tsx b/apps/admin-web/src/pages/HqLogsPage.tsx
index 16c32b9..bf552e5 100644
--- a/apps/admin-web/src/pages/HqLogsPage.tsx
+++ b/apps/admin-web/src/pages/HqLogsPage.tsx
@@ -5,6 +5,7 @@ import {
import type { ColumnsType } from 'antd/es/table';
import { useSearchParams } from 'react-router-dom';
import { request } from '../lib/api';
+import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants';
import { HQ_OPERATION_ACTION_OPTIONS, resolveHqOperationLabel } from '../lib/hq-log';
import { useAdminList } from '../lib/useAdminList';
@@ -55,14 +56,13 @@ export default function HqLogsPage() {
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
{
title: '操作人',
- width: 160,
+ width: 200,
+ ellipsis: true,
render: (_, r) => (
-
-
{r.hqName || '—'}
-
- {r.hqPhone || ''} {r.hqAccountId ? `(#${r.hqAccountId})` : ''}
-
-
+
),
},
{
@@ -170,7 +170,10 @@ export default function HqLogsPage() {
{detail.id}
{fmtTime(detail.createdAt)}
- {detail.hqName} / {detail.hqPhone} (ID: {detail.hqAccountId})
+
{detail.hqRole || '—'}
diff --git a/apps/admin-web/src/pages/PartnerLogsPage.tsx b/apps/admin-web/src/pages/PartnerLogsPage.tsx
index 9580874..02ba7a8 100644
--- a/apps/admin-web/src/pages/PartnerLogsPage.tsx
+++ b/apps/admin-web/src/pages/PartnerLogsPage.tsx
@@ -9,6 +9,7 @@ import {
type PartnerLogCategory,
} from '../lib/partner-log';
import { request } from '../lib/api';
+import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList';
@@ -71,21 +72,20 @@ export default function PartnerLogsPage() {
{
title: '合伙人',
width: 180,
+ ellipsis: true,
render: (_, r) => (
-
-
{r.companyName || '—'}
-
{r.partnerId}
-
+
),
},
{
title: '账号',
width: 150,
+ ellipsis: true,
render: (_, r) => (
-
-
{r.accountName || '—'}
-
{r.accountPhone || r.partnerAccountId || '—'}
-
+
),
},
{
diff --git a/apps/admin-web/src/pages/StoreLogsPage.tsx b/apps/admin-web/src/pages/StoreLogsPage.tsx
index 5f9e193..99fb995 100644
--- a/apps/admin-web/src/pages/StoreLogsPage.tsx
+++ b/apps/admin-web/src/pages/StoreLogsPage.tsx
@@ -9,6 +9,7 @@ import {
type StoreLogCategory,
} from '../lib/store-log';
import { request } from '../lib/api';
+import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList';
@@ -82,21 +83,16 @@ export default function StoreLogsPage() {
const columns: ColumnsType = [
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
{
- title: '门店', width: 180,
- render: (_, r) => (
-
-
{r.storeName || '—'}
-
{r.storeId}
-
- ),
+ title: '门店', width: 180, ellipsis: true,
+ render: (_, r) => ,
},
{
- title: '账号', width: 150,
+ title: '账号', width: 150, ellipsis: true,
render: (_, r) => (
-
-
{r.accountName || '—'}
-
{r.accountPhone || r.storeAccountId || '系统/HQ'}
-
+
),
},
{
diff --git a/apps/admin-web/src/pages/UserLogsPage.tsx b/apps/admin-web/src/pages/UserLogsPage.tsx
index 7073bb5..c66044e 100644
--- a/apps/admin-web/src/pages/UserLogsPage.tsx
+++ b/apps/admin-web/src/pages/UserLogsPage.tsx
@@ -4,6 +4,7 @@ import type { ColumnsType } from 'antd/es/table';
import { USER_LOG_CATEGORY_OPTIONS, resolveUserLogCategory, USER_LOG_CATEGORY_LABELS, type UserLogCategory } from '../lib/user-log';
import { useSearchParams } from 'react-router-dom';
import { request } from '../lib/api';
+import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList';
@@ -63,12 +64,12 @@ export default function UserLogsPage() {
const columns: ColumnsType = [
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
{
- title: '用户', width: 160,
+ title: '用户', width: 180, ellipsis: true,
render: (_, r) => (
-
-
{r.nickname || r.userNo || '—'}
-
{r.phone || r.userId || '—'}
-
+
),
},
{
diff --git a/apps/h5-shop/src/pages/HomePage.tsx b/apps/h5-shop/src/pages/HomePage.tsx
index cc17170..b0321f1 100644
--- a/apps/h5-shop/src/pages/HomePage.tsx
+++ b/apps/h5-shop/src/pages/HomePage.tsx
@@ -12,6 +12,7 @@ import {
sessionFromWechatLogin,
} from '../lib/wechat-auth';
import { isWechatEnv, weixinSdk } from '../lib/weixin';
+import { stripOAuthParamsFromLocation } from '@dukang/weixin-sdk';
import WechatScanAuthModal from '../components/WechatScanAuthModal';
const PENDING_SCAN_KEY = 'shop_pending_scan';
@@ -20,6 +21,14 @@ function formatMoney(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
}
+function formatScanError(e: unknown): string {
+ const msg = e instanceof Error ? e.message : '扫码失败,请重试';
+ if (/invalid signature/i.test(msg)) {
+ return '微信 JSSDK 签名校验失败:请确认公众号已配置 JS 接口安全域名为 shop.runxian.top,并刷新页面后重试';
+ }
+ return msg;
+}
+
export default function HomePage() {
const navigate = useNavigate();
const { applySession } = useStoreSession();
@@ -51,11 +60,12 @@ export default function HomePage() {
}
setAuthModalOpen(false);
setAuthError('');
+ stripOAuthParamsFromLocation();
setSearchParams({}, { replace: true });
const shouldScan = sessionStorage.getItem(PENDING_SCAN_KEY) === '1';
sessionStorage.removeItem(PENDING_SCAN_KEY);
if (shouldScan) {
- void runScan();
+ window.setTimeout(() => void runScan(), 0);
}
})
.catch((e) => {
@@ -81,7 +91,7 @@ export default function HomePage() {
}
navigate(`/redeem?token=${encodeURIComponent(token)}`);
} catch (e) {
- setScanMsg(e instanceof Error ? e.message : '扫码失败,请重试');
+ setScanMsg(formatScanError(e));
} finally {
setScanning(false);
}
diff --git a/packages/weixin-sdk/src/chooseImage.ts b/packages/weixin-sdk/src/chooseImage.ts
index 013d663..bfcced3 100644
--- a/packages/weixin-sdk/src/chooseImage.ts
+++ b/packages/weixin-sdk/src/chooseImage.ts
@@ -7,6 +7,39 @@ export type ChooseWechatImageOptions = {
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 = {
+ '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 {
const normalized = base64.startsWith('data:')
? base64
@@ -52,30 +85,58 @@ export async function chooseWechatImages(
if (platform === 'wechat-h5') {
const pageUrl = typeof window !== 'undefined' ? window.location.href.split('#')[0] : '';
- await ensureJssdkReady({
- apiBase: config.apiBase ?? '/api/v1',
- clientApp: config.clientApp,
- getAccessToken: config.getAccessToken,
- url: pageUrl,
- jsApiList: ['chooseImage', 'getLocalImgData'],
- });
+ const sourceTypeKey = sourceType.join(',');
+ try {
+ await ensureJssdkReady({
+ apiBase: config.apiBase ?? '/api/v1',
+ clientApp: config.clientApp,
+ 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) {
- throw new Error('微信选图接口不可用,请刷新页面后重试');
+ const errMsg = '微信选图接口不可用,请刷新页面后重试';
+ void reportChooseImageEvent(config, { status: 'fail', errMsg, sourceType: sourceTypeKey, stage: 'jssdk' });
+ throw new Error(errMsg);
}
- const localIds = await new Promise((resolve, reject) => {
- window.wx!.chooseImage!({
- count,
- sizeType: ['compressed'],
- sourceType,
- success: (res) => resolve(res.localIds ?? []),
- fail: (err) => reject(new Error(err.errMsg || '无法打开相册')),
+ let localIds: string[];
+ try {
+ localIds = await new Promise((resolve, reject) => {
+ window.wx!.chooseImage!({
+ count,
+ sizeType: ['compressed'],
+ 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[] = [];
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;
}
diff --git a/packages/weixin-sdk/src/index.ts b/packages/weixin-sdk/src/index.ts
index 3cec4f0..d94f1be 100644
--- a/packages/weixin-sdk/src/index.ts
+++ b/packages/weixin-sdk/src/index.ts
@@ -1,5 +1,5 @@
export { isWechatBrowser, isMiniProgram, getRuntimePlatform } from './env';
-export { initWechatJssdk, ensureJssdkReady, isJssdkReady } from './jssdk';
+export { initWechatJssdk, ensureJssdkReady, isJssdkReady, normalizeJssdkPageUrl, stripOAuthParamsFromLocation } from './jssdk';
export {
getWechatLocation,
getWechatLocationDetailed,
diff --git a/packages/weixin-sdk/src/jssdk.ts b/packages/weixin-sdk/src/jssdk.ts
index 7b56c36..ec3dd42 100644
--- a/packages/weixin-sdk/src/jssdk.ts
+++ b/packages/weixin-sdk/src/jssdk.ts
@@ -8,7 +8,32 @@ let configured = false;
let configuredUrl: string | null = null;
function currentPageUrl() {
- return typeof window !== 'undefined' ? window.location.href.split('#')[0] : '';
+ return typeof window !== 'undefined' ? normalizeJssdkPageUrl(window.location.href) : '';
+}
+
+/** 参与 JSSDK 签名的页面 URL(去掉 hash、OAuth 回跳参数) */
+export function normalizeJssdkPageUrl(rawUrl: string): string {
+ try {
+ const url = new URL(rawUrl);
+ url.hash = '';
+ url.searchParams.delete('code');
+ url.searchParams.delete('state');
+ const query = url.searchParams.toString();
+ return `${url.origin}${url.pathname}${query ? `?${query}` : ''}`;
+ } catch {
+ return rawUrl.split('#')[0];
+ }
+}
+
+export function stripOAuthParamsFromLocation(): void {
+ if (typeof window === 'undefined') return;
+ const url = new URL(window.location.href);
+ if (!url.searchParams.has('code') && !url.searchParams.has('state')) return;
+ url.searchParams.delete('code');
+ url.searchParams.delete('state');
+ const query = url.searchParams.toString();
+ const next = `${url.pathname}${query ? `?${query}` : ''}${url.hash}`;
+ window.history.replaceState({}, '', next);
}
function loadScript(): Promise {
diff --git a/server/dukang-api/src/integrations/wechat/wechat-log.util.ts b/server/dukang-api/src/integrations/wechat/wechat-log.util.ts
index 06959f6..9cd9132 100644
--- a/server/dukang-api/src/integrations/wechat/wechat-log.util.ts
+++ b/server/dukang-api/src/integrations/wechat/wechat-log.util.ts
@@ -5,6 +5,11 @@ export type WechatActorRef = {
refId: bigint;
};
+export function wechatActorRefFromAuth(actorType?: string, actorId?: bigint): WechatActorRef | undefined {
+ if (!actorType || actorId == null) return undefined;
+ return { refType: actorType, refId: actorId };
+}
+
type LogWechatAuthInput = {
scene: string;
requestUrl?: string;
diff --git a/server/dukang-api/src/modules/common/wechat-location.service.ts b/server/dukang-api/src/modules/common/wechat-location.service.ts
index 2ef7068..95fd68f 100644
--- a/server/dukang-api/src/modules/common/wechat-location.service.ts
+++ b/server/dukang-api/src/modules/common/wechat-location.service.ts
@@ -128,4 +128,29 @@ export class WechatLocationService {
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() };
+ }
}
diff --git a/server/dukang-api/src/modules/common/wechat.controller.ts b/server/dukang-api/src/modules/common/wechat.controller.ts
index 66ca90c..6589430 100644
--- a/server/dukang-api/src/modules/common/wechat.controller.ts
+++ b/server/dukang-api/src/modules/common/wechat.controller.ts
@@ -6,6 +6,7 @@ import { WECHAT_PROVIDER } from '../../integrations/integrations.constants';
import type { IWechatProvider } from '../../integrations/wechat/wechat.interface';
import { OptionalJwtAuthGuard } from '../../common/guards/optional-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';
class PhoneNumberDto {
@@ -41,6 +42,29 @@ class WechatLocationDto {
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')
export class WechatController {
constructor(
@@ -51,13 +75,27 @@ export class WechatController {
@Get('jssdk-config')
async jssdkConfig(@Query('url') url: string, @Req() req: Request) {
if (!url) throw new BadRequestException('url 参数必填');
- const pageUrl = decodeURIComponent(url).split('#')[0];
+ const decoded = decodeURIComponent(url).split('#')[0];
+ const pageUrl = this.normalizeJssdkUrl(decoded);
const user = (req as Request & { user?: AuthUser }).user;
const actorRef =
user?.actorType === 'USER' ? { refType: 'USER', refId: user.actorId } : undefined;
return this.wechat.createJssdkConfig(pageUrl, actorRef);
}
+ private normalizeJssdkUrl(rawUrl: string): string {
+ try {
+ const parsed = new URL(rawUrl);
+ parsed.hash = '';
+ parsed.searchParams.delete('code');
+ parsed.searchParams.delete('state');
+ const query = parsed.searchParams.toString();
+ return `${parsed.origin}${parsed.pathname}${query ? `?${query}` : ''}`;
+ } catch {
+ return rawUrl;
+ }
+ }
+
@Get('oauth-url')
oauthUrl(
@Query('redirectUri') redirectUri: string,
@@ -91,4 +129,18 @@ export class WechatController {
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),
+ });
+ }
}