门店页面修改
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
type WechatScanAuthModalProps = {
|
||||
open: boolean;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
onAuthorize: () => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export default function WechatScanAuthModal({
|
||||
open,
|
||||
loading,
|
||||
error,
|
||||
onAuthorize,
|
||||
onCancel,
|
||||
}: WechatScanAuthModalProps) {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="shop-scan-auth-overlay" role="dialog" aria-modal="true" aria-labelledby="shop-scan-auth-title">
|
||||
<div className="shop-scan-auth-card">
|
||||
<div className="shop-scan-auth-icon">
|
||||
<span className="material-symbols-outlined shop-fill-icon">qr_code_scanner</span>
|
||||
</div>
|
||||
<h2 id="shop-scan-auth-title" className="shop-scan-auth-title">微信授权</h2>
|
||||
<p className="shop-scan-auth-desc">
|
||||
扫码核销需使用微信相机扫描用户核销码。请先完成微信授权并允许使用摄像头。
|
||||
</p>
|
||||
{error && <p className="shop-scan-auth-error" role="alert">{error}</p>}
|
||||
<div className="shop-scan-auth-actions">
|
||||
<button type="button" className="shop-scan-auth-cancel" onClick={onCancel} disabled={loading}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" className="shop-scan-auth-confirm" onClick={onAuthorize} disabled={loading}>
|
||||
{loading ? '跳转授权中…' : '微信授权'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/** 从微信扫码结果解析核销 token(32 位 hex 或带 token 参数的 URL) */
|
||||
export function parseRedeemTokenFromScan(raw: string): string | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
if (/^[a-f0-9]{32}$/i.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
try {
|
||||
const url = trimmed.startsWith('http') ? new URL(trimmed) : new URL(trimmed, 'https://local.invalid');
|
||||
const fromQuery = url.searchParams.get('token');
|
||||
if (fromQuery && /^[a-f0-9]{32}$/i.test(fromQuery)) {
|
||||
return fromQuery.toLowerCase();
|
||||
}
|
||||
} catch {
|
||||
/* not a URL */
|
||||
}
|
||||
|
||||
const hexMatch = trimmed.match(/[a-f0-9]{32}/i);
|
||||
return hexMatch ? hexMatch[0].toLowerCase() : null;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { WechatLoginResult } from '@dukang/shared-types';
|
||||
import { isWechatEnv, weixinSdk } from './weixin';
|
||||
import { request, saveAuth, type ShopSessionPayload } from './api';
|
||||
|
||||
export type ShopAccountProfile = {
|
||||
id: string;
|
||||
storeId: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
wxOpenId?: string | null;
|
||||
store?: { name: string };
|
||||
};
|
||||
|
||||
export async function fetchShopAccount(): Promise<ShopAccountProfile> {
|
||||
return request<ShopAccountProfile>('SHOP_H5', '/shop/auth/me');
|
||||
}
|
||||
|
||||
export function needsWechatAuth(profile: ShopAccountProfile | null): boolean {
|
||||
return isWechatEnv() && !!profile && !profile.wxOpenId;
|
||||
}
|
||||
|
||||
export function sessionFromWechatLogin(result: WechatLoginResult): ShopSessionPayload | null {
|
||||
if (!result.accessToken || !result.refreshToken) return null;
|
||||
const store = result.store;
|
||||
return {
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken,
|
||||
store: store
|
||||
? {
|
||||
id: String(store.id ?? ''),
|
||||
storeId: String(store.storeId ?? ''),
|
||||
name: String(store.name ?? ''),
|
||||
phone: String(store.phone ?? ''),
|
||||
storeName: String(store.storeName ?? store.name ?? ''),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function saveShopWechatAuth(result: WechatLoginResult): boolean {
|
||||
const session = sessionFromWechatLogin(result);
|
||||
if (!session) return false;
|
||||
saveAuth(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function authorizeShopWechat(): Promise<WechatLoginResult | void> {
|
||||
if (!isWechatEnv()) {
|
||||
throw new Error('请在微信内打开以完成授权');
|
||||
}
|
||||
return weixinSdk.login();
|
||||
}
|
||||
|
||||
export async function handleShopWechatCallback(): Promise<WechatLoginResult | null> {
|
||||
if (!isWechatEnv()) return null;
|
||||
return weixinSdk.handleOAuthCallback();
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export const weixinSdk = createWeixinSdk({
|
||||
apiBase: '/api/v1',
|
||||
clientApp: 'SHOP_H5',
|
||||
getAccessToken: () => localStorage.getItem('accessToken'),
|
||||
wechatLoginPath: '/shop/auth/login/wechat',
|
||||
});
|
||||
|
||||
export { isWechatEnv };
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useStoreSession } from '../contexts/StoreSessionContext';
|
||||
import { request } from '../lib/api';
|
||||
import { parseRedeemTokenFromScan } from '../lib/redeem-scan';
|
||||
import {
|
||||
authorizeShopWechat,
|
||||
fetchShopAccount,
|
||||
handleShopWechatCallback,
|
||||
needsWechatAuth,
|
||||
saveShopWechatAuth,
|
||||
sessionFromWechatLogin,
|
||||
} from '../lib/wechat-auth';
|
||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
||||
import WechatScanAuthModal from '../components/WechatScanAuthModal';
|
||||
|
||||
const PENDING_SCAN_KEY = 'shop_pending_scan';
|
||||
|
||||
function formatMoney(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
||||
@@ -9,37 +22,107 @@ function formatMoney(n: number) {
|
||||
|
||||
export default function HomePage() {
|
||||
const navigate = useNavigate();
|
||||
const { applySession } = useStoreSession();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [dash, setDash] = useState<Record<string, unknown> | null>(null);
|
||||
const [open, setOpen] = useState(true);
|
||||
const [scanMsg, setScanMsg] = useState('');
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [authModalOpen, setAuthModalOpen] = useState(false);
|
||||
const [authLoading, setAuthLoading] = useState(false);
|
||||
const [authError, setAuthError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
request('SHOP_H5', '/shop/dashboard').then((d) => {
|
||||
request<Record<string, unknown>>('SHOP_H5', '/shop/dashboard').then((d) => {
|
||||
setDash(d);
|
||||
setOpen(String((d.store as Record<string, unknown>)?.status) === 'OPEN');
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWechatEnv() || !searchParams.get('code')) return;
|
||||
void handleShopWechatCallback()
|
||||
.then((result) => {
|
||||
if (!result) return;
|
||||
const session = sessionFromWechatLogin(result);
|
||||
if (session) {
|
||||
saveShopWechatAuth(result);
|
||||
applySession(session);
|
||||
}
|
||||
setAuthModalOpen(false);
|
||||
setAuthError('');
|
||||
setSearchParams({}, { replace: true });
|
||||
const shouldScan = sessionStorage.getItem(PENDING_SCAN_KEY) === '1';
|
||||
sessionStorage.removeItem(PENDING_SCAN_KEY);
|
||||
if (shouldScan) {
|
||||
void runScan();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
setAuthError(e instanceof Error ? e.message : '微信授权失败');
|
||||
});
|
||||
}, [searchParams, applySession, setSearchParams]);
|
||||
|
||||
async function runScan() {
|
||||
if (!isWechatEnv()) {
|
||||
navigate('/redeem');
|
||||
return;
|
||||
}
|
||||
setScanning(true);
|
||||
setScanMsg('');
|
||||
try {
|
||||
await weixinSdk.init();
|
||||
const raw = await weixinSdk.scanQrCode();
|
||||
if (!raw) return;
|
||||
const token = parseRedeemTokenFromScan(raw);
|
||||
if (!token) {
|
||||
setScanMsg('无法识别核销码,请扫描用户出示的核销二维码');
|
||||
return;
|
||||
}
|
||||
navigate(`/redeem?token=${encodeURIComponent(token)}`);
|
||||
} catch (e) {
|
||||
setScanMsg(e instanceof Error ? e.message : '扫码失败,请重试');
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScan() {
|
||||
setScanMsg('');
|
||||
if (!isWechatEnv()) {
|
||||
navigate('/redeem');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const profile = await fetchShopAccount();
|
||||
if (needsWechatAuth(profile)) {
|
||||
setAuthModalOpen(true);
|
||||
return;
|
||||
}
|
||||
await runScan();
|
||||
} catch (e) {
|
||||
setScanMsg(e instanceof Error ? e.message : '无法发起扫码');
|
||||
}
|
||||
}
|
||||
|
||||
async function startWechatAuth() {
|
||||
setAuthLoading(true);
|
||||
setAuthError('');
|
||||
try {
|
||||
sessionStorage.setItem(PENDING_SCAN_KEY, '1');
|
||||
await authorizeShopWechat();
|
||||
} catch (e) {
|
||||
sessionStorage.removeItem(PENDING_SCAN_KEY);
|
||||
setAuthError(e instanceof Error ? e.message : '微信授权失败');
|
||||
setAuthLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const store = dash?.store as Record<string, unknown> | undefined;
|
||||
const recent = (dash?.recentRecords as Array<Record<string, unknown>>) || [];
|
||||
const openTime = String(store?.openTime || '10:00');
|
||||
const closeTime = String(store?.closeTime || '22:00');
|
||||
|
||||
async function handleScan() {
|
||||
if (isWechatEnv()) {
|
||||
try {
|
||||
await weixinSdk.init();
|
||||
const token = await weixinSdk.scanQrCode();
|
||||
if (token) {
|
||||
navigate(`/redeem?token=${encodeURIComponent(token)}`);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* fall through to manual redeem page */
|
||||
}
|
||||
}
|
||||
navigate('/redeem');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shop-home-page">
|
||||
<header className="shop-home-header">
|
||||
@@ -68,10 +151,16 @@ export default function HomePage() {
|
||||
</section>
|
||||
|
||||
<section className="shop-home-scan">
|
||||
<button type="button" className="shop-home-scan-btn" onClick={handleScan}>
|
||||
<button
|
||||
type="button"
|
||||
className="shop-home-scan-btn"
|
||||
disabled={scanning}
|
||||
onClick={() => void handleScan()}
|
||||
>
|
||||
<span className="material-symbols-outlined">qr_code_scanner</span>
|
||||
</button>
|
||||
<p className="shop-home-scan-label">扫码核销</p>
|
||||
<p className="shop-home-scan-label">{scanning ? '正在打开相机…' : '扫码核销'}</p>
|
||||
{scanMsg && <p className="shop-home-scan-msg" role="alert">{scanMsg}</p>}
|
||||
</section>
|
||||
|
||||
<section className="shop-home-status">
|
||||
@@ -117,6 +206,18 @@ export default function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<WechatScanAuthModal
|
||||
open={authModalOpen}
|
||||
loading={authLoading}
|
||||
error={authError}
|
||||
onAuthorize={() => void startWechatAuth()}
|
||||
onCancel={() => {
|
||||
setAuthModalOpen(false);
|
||||
setAuthError('');
|
||||
sessionStorage.removeItem(PENDING_SCAN_KEY);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ function maskPhone(phone: string) {
|
||||
return `${phone.slice(0, 3)} **** ${phone.slice(-4)}`;
|
||||
}
|
||||
|
||||
const DEV_DEFAULT_CODE = import.meta.env.DEV ? '123456' : '';
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const { applySession } = useStoreSession();
|
||||
@@ -18,8 +16,8 @@ export default function LoginPage() {
|
||||
const quick = params.get('quick') === '1';
|
||||
const savedProfile = getStoreProfile();
|
||||
const [phone, setPhone] = useState(getLastPhone());
|
||||
const [code, setCode] = useState(DEV_DEFAULT_CODE);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
const [agreed, setAgreed] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [codeCooldown, setCodeCooldown] = useState(0);
|
||||
@@ -43,7 +41,7 @@ export default function LoginPage() {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone, scene: 'STORE_LOGIN' }),
|
||||
});
|
||||
setMsg(import.meta.env.DEV ? '验证码已发送(Mock: 123456)' : '验证码已发送');
|
||||
setMsg('验证码已发送');
|
||||
setCodeCooldown(60);
|
||||
const timer = setInterval(() => {
|
||||
setCodeCooldown((c) => {
|
||||
@@ -202,7 +200,7 @@ export default function LoginPage() {
|
||||
type="button"
|
||||
className="shop-login-submit"
|
||||
disabled={loading}
|
||||
onClick={login}
|
||||
onClick={() => void login()}
|
||||
>
|
||||
<span>{loading ? '登录中...' : '登录'}</span>
|
||||
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function RedeemConfirmPage() {
|
||||
return;
|
||||
}
|
||||
if (!token.trim()) {
|
||||
setMsg('请在开发者选项中输入核销码');
|
||||
setMsg('请先扫码获取核销码');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
+110
-2
@@ -152,23 +152,31 @@
|
||||
.shop-login-code-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.shop-login-code-row .shop-login-input-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shop-login-code-btn {
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0 16px;
|
||||
padding: 0 14px;
|
||||
background: #ffdad7;
|
||||
color: var(--color-heritage-red);
|
||||
font-family: var(--font-label);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -618,6 +626,106 @@
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.shop-home-scan-msg {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-heritage-red);
|
||||
text-align: center;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.shop-scan-auth-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.shop-scan-auth-card {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 28px 24px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-card);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shop-scan-auth-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 0 auto 16px;
|
||||
border-radius: 50%;
|
||||
background: rgba(166, 29, 36, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.shop-scan-auth-icon .material-symbols-outlined {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.shop-scan-auth-title {
|
||||
margin: 0 0 8px;
|
||||
font-family: var(--font-headline);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
.shop-scan-auth-desc {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-on-surface-variant);
|
||||
}
|
||||
|
||||
.shop-scan-auth-error {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.shop-scan-auth-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.shop-scan-auth-cancel,
|
||||
.shop-scan-auth-confirm {
|
||||
flex: 1;
|
||||
min-height: 44px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-label);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shop-scan-auth-cancel {
|
||||
border: 1px solid var(--color-outline-variant);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-on-surface);
|
||||
}
|
||||
|
||||
.shop-scan-auth-confirm {
|
||||
border: none;
|
||||
background: var(--color-heritage-red);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.shop-scan-auth-cancel:disabled,
|
||||
.shop-scan-auth-confirm:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.shop-home-status {
|
||||
background: var(--color-card);
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
@@ -23,16 +23,28 @@ export async function scanQrCode(config: WeixinSdkConfig): Promise<string | null
|
||||
clientApp: config.clientApp,
|
||||
getAccessToken: config.getAccessToken,
|
||||
});
|
||||
if (window.wx?.scanQRCode) {
|
||||
return new Promise((resolve) => {
|
||||
if (!window.wx?.scanQRCode) {
|
||||
throw new Error('当前微信版本不支持扫码,请升级微信后重试');
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
window.wx!.scanQRCode!({
|
||||
needResult: 1,
|
||||
scanType: ['qrCode', 'barCode'],
|
||||
success: (res) => resolve(res.resultStr || null),
|
||||
fail: () => resolve(null),
|
||||
});
|
||||
});
|
||||
fail: (res) => {
|
||||
const msg = res.errMsg || '扫码失败';
|
||||
if (/cancel/i.test(msg)) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
if (/permission|auth|denied|授权|拒绝|camera/i.test(msg)) {
|
||||
reject(new Error('相机权限未开启,请在微信设置中允许使用摄像头'));
|
||||
return;
|
||||
}
|
||||
reject(new Error(msg));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const manual = typeof window !== 'undefined' ? window.prompt('当前环境无法调起微信扫码,请手动输入核销码') : null;
|
||||
|
||||
@@ -112,7 +112,17 @@ export class ShopAuthController {
|
||||
}
|
||||
|
||||
@Post('login/wechat')
|
||||
wechatLogin(@Body() dto: LoginWechatDto) {
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
wechatLogin(@Req() req: Request, @Body() dto: LoginWechatDto) {
|
||||
const user = (req as Request & { user?: AuthUser }).user;
|
||||
if (user?.actorType === 'STORE') {
|
||||
return this.authService.bindStoreWechat(
|
||||
user.actorId,
|
||||
dto.code,
|
||||
ClientApp.SHOP_H5,
|
||||
dto.platform ?? 'h5',
|
||||
);
|
||||
}
|
||||
return this.authService.loginStoreWechat(dto.code, ClientApp.SHOP_H5, dto.platform ?? 'h5');
|
||||
}
|
||||
|
||||
|
||||
@@ -837,6 +837,50 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
async bindStoreWechat(
|
||||
storeAccountId: bigint,
|
||||
code: string,
|
||||
clientApp: ClientApp,
|
||||
platform: 'h5' | 'mini' = 'h5',
|
||||
) {
|
||||
this.assertWechatEnabled();
|
||||
const session =
|
||||
platform === 'mini'
|
||||
? await this.wechatProvider.code2Session(code)
|
||||
: await this.wechatProvider.oauth2AccessToken(code);
|
||||
|
||||
const account = await this.prisma.storeAccount.findUnique({
|
||||
where: { id: storeAccountId },
|
||||
include: { store: true },
|
||||
});
|
||||
if (!account) throw new BadRequestException('门店账号不存在');
|
||||
|
||||
const conflict = await this.prisma.storeAccount.findFirst({
|
||||
where: { wxOpenId: session.openId, id: { not: storeAccountId } },
|
||||
});
|
||||
if (conflict) {
|
||||
throw new BadRequestException('该微信已绑定其他门店账号');
|
||||
}
|
||||
|
||||
const updated = await this.prisma.storeAccount.update({
|
||||
where: { id: storeAccountId },
|
||||
data: {
|
||||
wxOpenId: session.openId,
|
||||
wxUnionId: session.unionId ?? account.wxUnionId,
|
||||
lastLoginAt: new Date(),
|
||||
},
|
||||
include: { store: true },
|
||||
});
|
||||
|
||||
return this.issueToken('STORE', updated.id, clientApp, false, undefined, {
|
||||
id: updated.id.toString(),
|
||||
storeId: updated.storeId.toString(),
|
||||
name: updated.name,
|
||||
phone: updated.phone,
|
||||
storeName: updated.store.name,
|
||||
});
|
||||
}
|
||||
|
||||
async bindPartnerWechat(
|
||||
partnerAccountId: bigint,
|
||||
code: string,
|
||||
|
||||
Reference in New Issue
Block a user