fix(mini-user): address review blockers
CI / verify (pull_request) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-21 13:03:02 +08:00
parent eb96b36d0b
commit bdf80e577b
14 changed files with 280 additions and 106 deletions
+1 -18
View File
@@ -1,6 +1,6 @@
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import { ClientApp } from '@dukang/shared-types'; import { ClientApp } from '@dukang/shared-types';
import { goLogin, forceReloadAfterAccountMerge } from './auth-nav'; import { forceReloadAfterAccountMerge } from './auth-nav';
function resolveApiBase(): string { function resolveApiBase(): string {
const origin = const origin =
@@ -54,10 +54,6 @@ export function isLoggedIn(): boolean {
return !!getToken(); return !!getToken();
} }
export function redirectToLogin() {
goLogin();
}
export function logout() { export function logout() {
clearAuth(); clearAuth();
Taro.reLaunch({ url: '/pages/home/index' }); Taro.reLaunch({ url: '/pages/home/index' });
@@ -76,17 +72,6 @@ function parseBody(data: unknown): { code?: number; message?: string } {
return {}; return {};
} }
function isOnLoginPage(): boolean {
try {
const pages = Taro.getCurrentPages();
const cur = pages[pages.length - 1] as { route?: string } | undefined;
const route = cur?.route || '';
return route.includes('pages/login');
} catch {
return false;
}
}
/** 统一请求:注入 CLIENT_APP + Bearer,解包 { code, message, data } */ /** 统一请求:注入 CLIENT_APP + Bearer,解包 { code, message, data } */
export async function request<T = unknown>(path: string, options: ReqOptions = {}): Promise<T> { export async function request<T = unknown>(path: string, options: ReqOptions = {}): Promise<T> {
const header: Record<string, string> = { const header: Record<string, string> = {
@@ -115,8 +100,6 @@ export async function request<T = unknown>(path: string, options: ReqOptions = {
if (/账号已合并/.test(mergedMsg)) { if (/账号已合并/.test(mergedMsg)) {
// 合并后旧 JWT 失效:强制刷新,不引导「重新登录」 // 合并后旧 JWT 失效:强制刷新,不引导「重新登录」
forceReloadAfterAccountMerge(); forceReloadAfterAccountMerge();
} else if (!isOnLoginPage()) {
redirectToLogin();
} }
} }
throw new Error(body?.message || '登录已过期,请重新登录'); throw new Error(body?.message || '登录已过期,请重新登录');
+17 -2
View File
@@ -7,6 +7,14 @@ const TAB_PAGES = new Set([
'/pages/mine/index', '/pages/mine/index',
]); ]);
let loginNavigationPending = false;
function isLoginPageActive(): boolean {
const pages = Taro.getCurrentPages();
const current = pages[pages.length - 1] as { route?: string } | undefined;
return !!current?.route?.includes('pages/login/');
}
function currentPagePath(): string { function currentPagePath(): string {
const pages = Taro.getCurrentPages(); const pages = Taro.getCurrentPages();
const cur = pages[pages.length - 1] as const cur = pages[pages.length - 1] as
@@ -25,6 +33,7 @@ function currentPagePath(): string {
/** 跳转登录页;默认带回当前页作为 return */ /** 跳转登录页;默认带回当前页作为 return */
export function goLogin(returnPath?: string, extras?: Record<string, string>) { export function goLogin(returnPath?: string, extras?: Record<string, string>) {
if (loginNavigationPending || isLoginPageActive()) return;
const returnTo = returnPath ?? currentPagePath(); const returnTo = returnPath ?? currentPagePath();
const parts: string[] = []; const parts: string[] = [];
if (returnTo) parts.push(`return=${encodeURIComponent(returnTo)}`); if (returnTo) parts.push(`return=${encodeURIComponent(returnTo)}`);
@@ -34,8 +43,14 @@ export function goLogin(returnPath?: string, extras?: Record<string, string>) {
} }
} }
const url = parts.length ? `/pages/login/index?${parts.join('&')}` : '/pages/login/index'; const url = parts.length ? `/pages/login/index?${parts.join('&')}` : '/pages/login/index';
Taro.navigateTo({ url }).catch(() => { loginNavigationPending = true;
Taro.redirectTo({ url }); void Taro.navigateTo({ url })
.catch(() => Taro.redirectTo({ url }))
.finally(() => {
// 等路由栈稳定后再释放,拦截同一轮请求触发的重复登录跳转。
setTimeout(() => {
loginNavigationPending = false;
}, 500);
}); });
} }
+36 -8
View File
@@ -6,6 +6,17 @@ export type MiniWechatProfile = {
avatarUrl?: string; avatarUrl?: string;
}; };
export type MiniWechatProfileUpdate = MiniWechatProfile & {
avatarResourceId?: string;
};
export type UploadedAvatarResource = {
resourceId: string;
url: string;
bucket: string;
ossKey: string;
};
const WX_PROFILE_CACHE_KEY = 'wx_mini_profile_cache'; const WX_PROFILE_CACHE_KEY = 'wx_mini_profile_cache';
export function cacheWxProfile(info: MiniWechatProfile) { export function cacheWxProfile(info: MiniWechatProfile) {
@@ -55,8 +66,8 @@ export function mergeWxDisplayProfile(profile: UserProfile): UserProfile {
}; };
} }
/** 上传 chooseAvatar 临时文件到 OSS,返回永久 URL */ /** 上传 chooseAvatar 临时文件到 OSS返回已登记到当前用户的真实资源。 */
export async function uploadAvatarTempFile(tempFilePath: string): Promise<string> { export async function uploadAvatarTempFile(tempFilePath: string): Promise<UploadedAvatarResource> {
const { API_BASE, CLIENT_APP, getToken } = await import('./api'); const { API_BASE, CLIENT_APP, getToken } = await import('./api');
const token = getToken(); const token = getToken();
if (!token) throw new Error('请先登录'); if (!token) throw new Error('请先登录');
@@ -75,7 +86,11 @@ export async function uploadAvatarTempFile(tempFilePath: string): Promise<string
}, },
}); });
let body: { code?: number; message?: string; data?: { url?: string } } = {}; let body: {
code?: number;
message?: string;
data?: { resourceId?: string; url?: string; bucket?: string; ossKey?: string };
} = {};
try { try {
body = JSON.parse(String(res.data || '{}')) as typeof body; body = JSON.parse(String(res.data || '{}')) as typeof body;
} catch { } catch {
@@ -84,14 +99,26 @@ export async function uploadAvatarTempFile(tempFilePath: string): Promise<string
if (res.statusCode === 401 || body.code === 401) { if (res.statusCode === 401 || body.code === 401) {
throw new Error(body.message || '登录已过期,请重新登录'); throw new Error(body.message || '登录已过期,请重新登录');
} }
if (res.statusCode >= 400 || body.code !== 0 || !body.data?.url) { if (
res.statusCode >= 400 ||
body.code !== 0 ||
!body.data?.resourceId ||
!body.data.url ||
!body.data.bucket ||
!body.data.ossKey
) {
throw new Error(body.message || '头像上传失败'); throw new Error(body.message || '头像上传失败');
} }
return body.data.url; return {
resourceId: body.data.resourceId,
url: body.data.url,
bucket: body.data.bucket,
ossKey: body.data.ossKey,
};
} }
export async function uploadMiniWechatProfile(info: MiniWechatProfile): Promise<UserProfile | null> { export async function uploadMiniWechatProfile(info: MiniWechatProfileUpdate): Promise<UserProfile | null> {
if (!info.nickname && !info.avatarUrl) return null; if (!info.nickname && !info.avatarUrl && !info.avatarResourceId) return null;
const { request } = await import('./api'); const { request } = await import('./api');
const updated = await request<UserProfile>('/auth/wechat/mini-profile', { const updated = await request<UserProfile>('/auth/wechat/mini-profile', {
method: 'POST', method: 'POST',
@@ -124,6 +151,7 @@ export async function syncMiniWechatProfile(
if (process.env.TARO_ENV !== 'weapp') return null; if (process.env.TARO_ENV !== 'weapp') return null;
const info = prefetched?.nickname || prefetched?.avatarUrl ? prefetched : getCachedWxProfile(); const info = prefetched?.nickname || prefetched?.avatarUrl ? prefetched : getCachedWxProfile();
if (!info?.nickname && !info?.avatarUrl) return null; if (!info?.nickname && !info?.avatarUrl) return null;
await uploadMiniWechatProfile(info); // 缓存头像 URL 可能来自历史微信资料,未经过当前 OSS 上传登记;这里只同步昵称。
if (info.nickname) await uploadMiniWechatProfile({ nickname: info.nickname });
return info; return info;
} }
+22 -18
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { View, Text } from '@tarojs/components'; import { View, Text } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro'; import Taro, { useDidShow } from '@tarojs/taro';
import PageShell from '../../components/PageShell'; import PageShell from '../../components/PageShell';
@@ -21,9 +21,9 @@ type Product = {
}; };
const AROMA_TABS = [ const AROMA_TABS = [
{ key: 'QINGXIANG', label: '清香型', open: true }, { key: 'QINGXIANG', label: '清香型' },
{ key: 'JIANGXIANG', label: '酱香型', open: false }, { key: 'JIANGXIANG', label: '酱香型' },
{ key: 'NONGXIANG', label: '浓香型', open: false }, { key: 'NONGXIANG', label: '浓香型' },
] as const; ] as const;
export default function HomePage() { export default function HomePage() {
@@ -49,20 +49,26 @@ export default function HomePage() {
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [cityCode]); }, [cityCode]);
function onAromaTabClick(key: string, open: boolean) { const availableAromas = useMemo(
if (!open) { () =>
toast('暂未开放'); AROMA_TABS.filter((item) =>
return; products.some((product) => product.aromaType === item.key),
} ),
setTab(key); [products],
);
useEffect(() => {
if (loading || availableAromas.length === 0) return;
if (!availableAromas.some((item) => item.key === tab)) {
setTab(availableAromas[0].key);
} }
}, [availableAromas, loading, tab]);
function openProductDetail(id: string) { function openProductDetail(id: string) {
Taro.navigateTo({ url: `/pages/product-detail/index?id=${id}` }); Taro.navigateTo({ url: `/pages/product-detail/index?id=${id}` });
} }
const filtered = products.filter((p) => p.aromaType === tab); const filtered = products.filter((p) => p.aromaType === tab);
const onSale = tab === 'QINGXIANG';
return ( return (
<PageShell variant="tab" className="home-page no-tab-header"> <PageShell variant="tab" className="home-page no-tab-header">
@@ -70,11 +76,11 @@ export default function HomePage() {
<View className="home-aroma-nav"> <View className="home-aroma-nav">
<View className="home-aroma-tabs"> <View className="home-aroma-tabs">
{AROMA_TABS.map((t) => ( {availableAromas.map((t) => (
<Text <Text
key={t.key} key={t.key}
className={`home-aroma-tab${tab === t.key ? ' home-aroma-tab--active' : ''}${!t.open ? ' home-aroma-tab--muted' : ''}`} className={`home-aroma-tab${tab === t.key ? ' home-aroma-tab--active' : ''}`}
onClick={() => onAromaTabClick(t.key, t.open)} onClick={() => setTab(t.key)}
> >
{t.label} {t.label}
</Text> </Text>
@@ -85,12 +91,10 @@ export default function HomePage() {
<View className="home-product-list"> <View className="home-product-list">
{loading ? <View className="home-empty"></View> : null} {loading ? <View className="home-empty"></View> : null}
{!loading && !onSale ? <View className="home-empty">线</View> : null} {!loading && products.length === 0 ? (
{!loading && onSale && filtered.length === 0 ? ( <View className="home-empty"></View>
<View className="home-empty"></View>
) : null} ) : null}
{!loading && {!loading &&
onSale &&
filtered.map((p) => { filtered.map((p) => {
const images = getProductImages(p); const images = getProductImages(p);
return ( return (
+24
View File
@@ -150,6 +150,19 @@ export default function LoginPage() {
return true; return true;
} }
function cancelLogin() {
const pages = Taro.getCurrentPages();
if (pages.length > 1) {
Taro.navigateBack().catch(() => {
Taro.switchTab({ url: '/pages/home/index' });
});
return;
}
Taro.switchTab({ url: '/pages/home/index' }).catch(() => {
Taro.reLaunch({ url: '/pages/home/index' });
});
}
function applySessionAndLeave( function applySessionAndLeave(
data: SessionPayload | WechatLoginResult, data: SessionPayload | WechatLoginResult,
phoneValue?: string, phoneValue?: string,
@@ -370,6 +383,12 @@ export default function LoginPage() {
return ( return (
<PageShell variant="plain" className="login-page"> <PageShell variant="plain" className="login-page">
<View className="login-nav">
<View className="login-nav-back" onClick={cancelLogin}>
<Text className="login-nav-back-icon"></Text>
<Text></Text>
</View>
</View>
<View className="login-header"> <View className="login-header">
<View className="login-logo-wrap"> <View className="login-logo-wrap">
<View className="login-logo"> <View className="login-logo">
@@ -523,6 +542,11 @@ export default function LoginPage() {
<WechatLoginButton loading={wxLoading} label="授权登录" onClick={() => void wechatLogin()} /> <WechatLoginButton loading={wxLoading} label="授权登录" onClick={() => void wechatLogin()} />
</> </>
) : null} ) : null}
<View className="login-cancel-btn" onClick={cancelLogin}>
<Text></Text>
</View>
<Text className="login-cancel-hint"></Text>
</View> </View>
</PageShell> </PageShell>
); );
+47 -23
View File
@@ -50,11 +50,13 @@ export default function MinePage() {
const [draftAvatarUrl, setDraftAvatarUrl] = useState(''); const [draftAvatarUrl, setDraftAvatarUrl] = useState('');
const [draftNickname, setDraftNickname] = useState(''); const [draftNickname, setDraftNickname] = useState('');
const [savingProfile, setSavingProfile] = useState(false); const [savingProfile, setSavingProfile] = useState(false);
const [profileLoadError, setProfileLoadError] = useState('');
function resetGuestState() { function resetGuestState() {
setProfile(null); setProfile(null);
setBenefitBalance(0); setBenefitBalance(0);
setOrderCounts({}); setOrderCounts({});
setProfileLoadError('');
} }
function applyProfile(me: UserProfile) { function applyProfile(me: UserProfile) {
@@ -63,6 +65,7 @@ export default function MinePage() {
function loadProfile() { function loadProfile() {
if (!isLoggedIn()) return; if (!isLoggedIn()) return;
setProfileLoadError('');
Promise.all([ Promise.all([
request<UserProfile>('/auth/me'), request<UserProfile>('/auth/me'),
request<Array<Record<string, unknown>>>('/benefit/coupons').catch(() => []), request<Array<Record<string, unknown>>>('/benefit/coupons').catch(() => []),
@@ -83,7 +86,16 @@ export default function MinePage() {
}); });
setOrderCounts(counts); setOrderCounts(counts);
}) })
.catch(() => {}); .catch((error) => {
if (!isLoggedIn()) {
setAuthed(false);
resetGuestState();
return;
}
const message = error instanceof Error ? error.message : '个人资料加载失败';
setProfileLoadError(message);
toast('个人资料加载失败,请点击重试');
});
} }
useDidShow(() => { useDidShow(() => {
@@ -162,23 +174,21 @@ export default function MinePage() {
/** 点击头像:绑定账号后用 chooseAvatar + nickname 获取头像昵称 */ /** 点击头像:绑定账号后用 chooseAvatar + nickname 获取头像昵称 */
async function handleAvatarTap() { async function handleAvatarTap() {
if (bindingWx || savingProfile) return;
if (!isLoggedIn()) { if (!isLoggedIn()) {
goLogin('/pages/mine/index'); goLogin('/pages/mine/index');
return; return;
} }
if (!isWeapp) { if (isWeapp) {
openProfileSheet();
return;
}
if (!profile?.hasWechat) { if (!profile?.hasWechat) {
const ok = await ensureWechatBound(); const ok = await ensureWechatBound();
if (ok) loadProfile(); if (ok) loadProfile();
} }
return; return;
} }
if (!profile?.hasWechat) {
const ok = await ensureWechatBound();
if (!ok) return;
}
openProfileSheet();
}
async function onChooseAvatar(e: { detail?: { avatarUrl?: string } }) { async function onChooseAvatar(e: { detail?: { avatarUrl?: string } }) {
const tempPath = e.detail?.avatarUrl?.trim(); const tempPath = e.detail?.avatarUrl?.trim();
@@ -202,16 +212,18 @@ export default function MinePage() {
} }
setSavingProfile(true); setSavingProfile(true);
try { try {
if (!profile?.hasWechat) {
const ok = await ensureWechatBound();
if (!ok) return;
}
let avatarUrl = draftAvatarUrl; let avatarUrl = draftAvatarUrl;
let avatarResourceId: string | undefined;
if (draftAvatarTemp) { if (draftAvatarTemp) {
avatarUrl = await uploadAvatarTempFile(draftAvatarTemp); const uploaded = await uploadAvatarTempFile(draftAvatarTemp);
avatarUrl = uploaded.url;
avatarResourceId = uploaded.resourceId;
} }
const updated = await uploadMiniWechatProfile({ nickname, avatarUrl }); const updated = await uploadMiniWechatProfile({
if (updated) applyProfile({ ...updated, hasWechat: true }); nickname,
...(avatarResourceId ? { avatarUrl, avatarResourceId } : {}),
});
if (updated) applyProfile(updated);
setProfileSheetOpen(false); setProfileSheetOpen(false);
toast('头像昵称已更新', 'success'); toast('头像昵称已更新', 'success');
loadProfile(); loadProfile();
@@ -262,7 +274,9 @@ export default function MinePage() {
</View> </View>
</View> </View>
<View className="mine-login-gate"> <View className="mine-login-gate">
<View className="mine-login-gate-hint"></View> <View className="mine-login-gate-hint">
</View>
<View className="mine-login-btn" onClick={() => goLogin('/pages/mine/index')}> <View className="mine-login-btn" onClick={() => goLogin('/pages/mine/index')}>
<Text></Text> <Text></Text>
</View> </View>
@@ -283,9 +297,10 @@ export default function MinePage() {
} }
const nickname = display.nickname || '用户'; const nickname = display.nickname || '用户';
const needProfileFill = isWeapp && needsWxProfileFill(display); const needProfileFill = isWeapp && needsWxProfileFill(display);
const avatarProfileReady = isWeapp ? !needProfileFill : hasWechat;
const memberLabel = needProfileFill const memberLabel = needProfileFill
? '点击头像完善资料' ? '点击头像完善资料'
: hasWechat : isWeapp || hasWechat
? '好客会员' ? '好客会员'
: canWxAuth : canWxAuth
? '点击头像授权' ? '点击头像授权'
@@ -305,7 +320,7 @@ export default function MinePage() {
> >
<View <View
className={`mine-avatar${ className={`mine-avatar${
!needProfileFill && hasWechat ? ' mine-avatar--wx-ok' : ' mine-avatar--wx-pending' avatarProfileReady ? ' mine-avatar--wx-ok' : ' mine-avatar--wx-pending'
}`} }`}
> >
{renderAvatarContent(display.avatarUrl)} {renderAvatarContent(display.avatarUrl)}
@@ -313,13 +328,11 @@ export default function MinePage() {
{avatarClickable ? ( {avatarClickable ? (
<View <View
className={`mine-avatar-status${ className={`mine-avatar-status${
needProfileFill || !hasWechat avatarProfileReady ? ' mine-avatar-status--ok' : ' mine-avatar-status--pending'
? ' mine-avatar-status--pending'
: ' mine-avatar-status--ok'
}`} }`}
> >
<Text> <Text>
{bindingWx ? '授权中' : needProfileFill || !hasWechat ? '去完善' : '更换'} {bindingWx ? '授权中' : avatarProfileReady ? '更换' : '去完善'}
</Text> </Text>
</View> </View>
) : null} ) : null}
@@ -330,9 +343,20 @@ export default function MinePage() {
onClick={avatarClickable ? () => void handleAvatarTap() : undefined} onClick={avatarClickable ? () => void handleAvatarTap() : undefined}
> >
<Text className="mine-profile-name">{nickname}</Text> <Text className="mine-profile-name">{nickname}</Text>
<Text className={`mine-member-tag${hasWechat && !needProfileFill ? ' mine-member-tag--wechat' : ''}`}> <Text className={`mine-member-tag${avatarProfileReady ? ' mine-member-tag--wechat' : ''}`}>
{memberLabel} {memberLabel}
</Text> </Text>
{profileLoadError ? (
<Text
className="mine-profile-retry"
onClick={(event) => {
event.stopPropagation();
loadProfile();
}}
>
</Text>
) : null}
</View> </View>
</View> </View>
</View> </View>
-4
View File
@@ -81,10 +81,6 @@
border-bottom-color: var(--color-heritage-red); border-bottom-color: var(--color-heritage-red);
} }
.home-aroma-tab--muted {
opacity: 0.65;
}
.home-product-list { .home-product-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+48
View File
@@ -326,3 +326,51 @@
.login-agreement-link { .login-agreement-link {
color: var(--color-heritage-red); color: var(--color-heritage-red);
} }
.login-nav {
min-height: 44px;
padding: 8px var(--space-page) 0;
display: flex;
align-items: center;
box-sizing: border-box;
}
.login-nav-back {
min-width: 72px;
min-height: 40px;
display: flex;
align-items: center;
gap: 4px;
color: var(--color-on-surface);
font-size: 16px;
font-weight: 600;
}
.login-nav-back-icon {
font-size: 30px;
line-height: 1;
}
.login-cancel-btn {
width: 100%;
min-height: 48px;
margin-top: 20px;
border: 1px solid var(--color-heritage-red);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
color: var(--color-heritage-red);
font-size: 16px;
font-weight: 700;
background: #fff;
}
.login-cancel-hint {
display: block;
margin-top: 10px;
text-align: center;
color: var(--color-on-surface-variant);
font-size: 13px;
}
+8
View File
@@ -149,6 +149,14 @@
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
} }
.mine-profile-retry {
display: block;
margin-top: 6px;
font-size: 11px;
color: #fff;
text-decoration: underline;
}
.mine-main { .mine-main {
margin-top: -28px; margin-top: -28px;
position: relative; position: relative;
@@ -16,7 +16,7 @@ import { ClientConfigController } from './client-config.controller';
import { WechatLocationService } from './wechat-location.service'; import { WechatLocationService } from './wechat-location.service';
@Module({ @Module({
imports: [IamModule, IntegrationsModule, SystemConfigModule, forwardRef(() => AnalyticsModule)], imports: [forwardRef(() => IamModule), IntegrationsModule, SystemConfigModule, forwardRef(() => AnalyticsModule)],
controllers: [ controllers: [
ResourceController, ResourceController,
EventController, EventController,
@@ -102,6 +102,9 @@ export class ResourceService {
}); });
throw new BadRequestException(message); throw new BadRequestException(message);
} }
if (dto.bizType === 'AVATAR' && !file.mimetype?.startsWith('image/')) {
throw new BadRequestException('头像仅支持图片文件');
}
try { try {
const result = await this.oss.putObject({ const result = await this.oss.putObject({
@@ -125,6 +128,24 @@ export class ResourceService {
externalNo: result.ossKey, externalNo: result.ossKey,
status: 'SUCCESS', status: 'SUCCESS',
}); });
if (actor?.refType === 'USER' && dto.bizType === 'AVATAR' && dto.mediaType === 'IMAGE') {
const resource = await this.prisma.commonResource.create({
data: {
ownerType: 'USER',
ownerId: actor.refId,
bizType: 'AVATAR',
mediaType: 'IMAGE',
ossBucket: result.bucket,
ossKey: result.ossKey,
url: result.url,
fileName: file.originalname || 'avatar',
fileSize: BigInt(file.size),
mimeType: file.mimetype,
status: 'ACTIVE',
},
});
return serializeBigInt({ ...result, resourceId: resource.id });
}
return result; return result;
} catch (err) { } catch (err) {
await logOssUpload(this.prisma, { await logOssUpload(this.prisma, {
@@ -138,6 +159,37 @@ export class ResourceService {
} }
} }
async getOwnedActiveAvatar(resourceId: bigint, userId: bigint) {
const resource = await this.prisma.commonResource.findFirst({
where: {
id: resourceId,
ownerType: 'USER',
ownerId: userId,
bizType: 'AVATAR',
mediaType: 'IMAGE',
status: 'ACTIVE',
},
});
if (!resource) throw new BadRequestException('头像资源无效或不属于当前用户');
return resource;
}
async getOwnedActiveAvatarByUrl(url: string, userId: bigint) {
const resource = await this.prisma.commonResource.findFirst({
where: {
url,
ownerType: 'USER',
ownerId: userId,
bizType: 'AVATAR',
mediaType: 'IMAGE',
status: 'ACTIVE',
},
orderBy: { createdAt: 'desc' },
});
if (!resource) throw new BadRequestException('头像资源无效或不属于当前用户');
return resource;
}
async register(dto: RegisterResourceDto) { async register(dto: RegisterResourceDto) {
const resource = await this.prisma.commonResource.create({ const resource = await this.prisma.commonResource.create({
data: { data: {
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto';
import { import {
BadRequestException, BadRequestException,
ForbiddenException, ForbiddenException,
forwardRef,
Inject, Inject,
Injectable, Injectable,
NotFoundException, NotFoundException,
@@ -22,6 +23,7 @@ import { serializeBigInt } from '../../common/decorators/current-user.decorator'
import { verifyPassword } from '../../common/crypto/password.util'; import { verifyPassword } from '../../common/crypto/password.util';
import { AnalyticsService } from '../analytics/analytics.service'; import { AnalyticsService } from '../analytics/analytics.service';
import { UserAddressService } from './user-address.service'; import { UserAddressService } from './user-address.service';
import { ResourceService } from '../common/resource.service';
import type { User } from '@prisma/client'; import type { User } from '@prisma/client';
@@ -62,6 +64,7 @@ export class AuthService {
private readonly analyticsService: AnalyticsService, private readonly analyticsService: AnalyticsService,
private readonly smsCodeStore: SmsCodeStore, private readonly smsCodeStore: SmsCodeStore,
private readonly userAddressService: UserAddressService, private readonly userAddressService: UserAddressService,
@Inject(forwardRef(() => ResourceService)) private readonly resourceService: ResourceService,
) {} ) {}
private assertMobilePhone(phone: string) { private assertMobilePhone(phone: string) {
@@ -1407,12 +1410,9 @@ export class AuthService {
async updateMiniWechatProfile( async updateMiniWechatProfile(
userId: bigint, userId: bigint,
input: { nickname?: string; avatarUrl?: string }, input: { nickname?: string; avatarUrl?: string; avatarResourceId?: string },
) { ) {
const user = await this.assertActiveUser(userId); const user = await this.assertActiveUser(userId);
if (!user.wxOpenId) {
throw new BadRequestException('请先完成微信授权');
}
const data: { const data: {
nickname?: string; nickname?: string;
@@ -1425,37 +1425,23 @@ export class AuthService {
} }
const avatarUrl = input.avatarUrl?.trim(); const avatarUrl = input.avatarUrl?.trim();
if (avatarUrl) { const avatarResourceId = input.avatarResourceId?.trim();
if (user.avatarResourceId) { if (avatarResourceId) {
await this.prisma.commonResource.update({ let resourceId: bigint;
where: { id: user.avatarResourceId }, try {
data: { url: avatarUrl }, resourceId = BigInt(avatarResourceId);
}); } catch {
} else { throw new BadRequestException('头像资源编号无效');
const avatar = await this.prisma.commonResource.create({
data: {
ownerType: 'USER',
ownerId: userId,
bizType: 'AVATAR',
mediaType: 'IMAGE',
ossBucket: 'wechat',
ossKey: `wx-avatar/${user.wxOpenId}`,
url: avatarUrl,
status: 'ACTIVE',
},
});
data.avatarResourceId = avatar.id;
} }
const avatar = await this.resourceService.getOwnedActiveAvatar(resourceId, userId);
data.avatarResourceId = avatar.id;
} else if (avatarUrl) {
// 兼容已发布旧客户端:只接受刚由当前用户上传并登记过的真实资源 URL。
const avatar = await this.resourceService.getOwnedActiveAvatarByUrl(avatarUrl, userId);
data.avatarResourceId = avatar.id;
} }
if (!data.nickname && !data.avatarResourceId) { if (!data.nickname && !data.avatarResourceId) {
if (avatarUrl && user.avatarResourceId) {
const refreshed = await this.prisma.user.findUnique({
where: { id: userId },
include: { avatar: true },
});
return this.formatUserProfile(refreshed ?? user);
}
return this.formatUserProfile(user); return this.formatUserProfile(user);
} }
@@ -118,6 +118,10 @@ export class MiniWechatProfileDto {
@IsString() @IsString()
@IsOptional() @IsOptional()
avatarUrl?: string; avatarUrl?: string;
@IsString()
@IsOptional()
avatarResourceId?: string;
} }
export class CheckPartnerPhoneDto { export class CheckPartnerPhoneDto {
@@ -24,10 +24,12 @@ import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard'; import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
import { ShopStoreGuard } from '../../common/guards/shop-store.guard'; import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
import { StoreMembershipService } from '../../common/guards/store-membership.service'; import { StoreMembershipService } from '../../common/guards/store-membership.service';
import { CommonModule } from '../common/common.module';
@Module({ @Module({
imports: [ imports: [
IntegrationsModule, IntegrationsModule,
forwardRef(() => CommonModule),
forwardRef(() => AnalyticsModule), forwardRef(() => AnalyticsModule),
JwtModule.register({ JwtModule.register({
secret: process.env.JWT_SECRET || 'dukang-prev1-dev-secret', secret: process.env.JWT_SECRET || 'dukang-prev1-dev-secret',