feat(admin): HQ手机号编辑、合伙人子账号日志与子账号H5体验
支持 HQ 账户改手机号(格式校验)、合伙人日志包含子账号且子账号不显示主账号信息;改手机号时清除微信绑定。合伙人 H5 子账号页签与我的页、录入门店底部按钮及上传失败提示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -77,6 +77,7 @@ export default function HqAccountsPage() {
|
||||
setDetail(row);
|
||||
editForm.setFieldsValue({
|
||||
name: row.name,
|
||||
phone: row.phone,
|
||||
loginName: row.loginName,
|
||||
adminRole: row.adminRole,
|
||||
status: row.status,
|
||||
@@ -128,6 +129,16 @@ export default function HqAccountsPage() {
|
||||
}>
|
||||
<Form form={editForm} layout="vertical">
|
||||
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label="手机号"
|
||||
rules={[
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
|
||||
]}
|
||||
>
|
||||
<Input maxLength={11} />
|
||||
</Form.Item>
|
||||
<Form.Item name="loginName" label="用户名"><Input placeholder="用于密码登录" /></Form.Item>
|
||||
<Form.Item name="password" label="新密码" extra="留空则不修改">
|
||||
<Input.Password placeholder="至少 6 位" />
|
||||
|
||||
@@ -5,9 +5,11 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
PARTNER_LOG_CATEGORY_OPTIONS,
|
||||
PARTNER_LOG_CATEGORY_LABELS,
|
||||
PARTNER_STAFF_ROLE_LABELS,
|
||||
resolvePartnerLogCategory,
|
||||
type PartnerLogCategory,
|
||||
} from '../lib/partner-log';
|
||||
type PartnerStaffRole,
|
||||
} from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { AdminCellLine } from '../components/AdminCellLine';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
@@ -20,6 +22,8 @@ type Row = {
|
||||
accountName: string | null;
|
||||
accountPhone: string | null;
|
||||
companyName: string | null;
|
||||
isSubAccount?: boolean;
|
||||
staffRole?: string | null;
|
||||
category: PartnerLogCategory | null;
|
||||
eventName: string;
|
||||
clientApp: string | null;
|
||||
@@ -74,7 +78,11 @@ export default function PartnerLogsPage() {
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render: (_, r) => (
|
||||
r.isSubAccount ? (
|
||||
<Tag color="blue">子账号</Tag>
|
||||
) : (
|
||||
<AdminCellLine primary={r.companyName} secondary={r.partnerId} />
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -84,7 +92,11 @@ export default function PartnerLogsPage() {
|
||||
render: (_, r) => (
|
||||
<AdminCellLine
|
||||
primary={r.accountName}
|
||||
secondary={r.accountPhone || r.partnerAccountId}
|
||||
secondary={
|
||||
r.isSubAccount && r.staffRole
|
||||
? `${r.accountPhone || ''} · ${PARTNER_STAFF_ROLE_LABELS[r.staffRole as PartnerStaffRole] || r.staffRole}`
|
||||
: r.accountPhone || r.partnerAccountId
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import StoreCreatePage from './pages/StoreCreatePage';
|
||||
import OrderListPage from './pages/OrderListPage';
|
||||
import OrderDetailPage from './pages/OrderDetailPage';
|
||||
import CenterPage from './pages/CenterPage';
|
||||
import PartnerMePage from './pages/PartnerMePage';
|
||||
import BillsPage from './pages/BillsPage';
|
||||
import SettlementPage from './pages/SettlementPage';
|
||||
import ReshipPage from './pages/ReshipPage';
|
||||
@@ -17,6 +18,7 @@ import WeeklyReportPage from './pages/WeeklyReportPage';
|
||||
import ProxyOrderPage from './pages/ProxyOrderPage';
|
||||
import StaffListPage from './pages/StaffListPage';
|
||||
import StaffCreatePage from './pages/StaffCreatePage';
|
||||
import LeaderboardPage from './pages/LeaderboardPage';
|
||||
|
||||
function PrimaryRoutes() {
|
||||
return (
|
||||
@@ -50,6 +52,7 @@ function SubAccountRoutes() {
|
||||
<Route path="/stores" element={<StoreListPage />} />
|
||||
<Route path="/stores/new" element={<StoreCreatePage />} />
|
||||
<Route path="/stores/:id" element={<StoreDetailPage />} />
|
||||
<Route path="/me" element={<PartnerMePage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/stores/new?step=1" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../lib/wechat-auth';
|
||||
import { formatChooseImageFailMessage } from '@dukang/weixin-sdk';
|
||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
||||
import { toastError } from '../lib/toast';
|
||||
import type { ClientRuntimeConfig } from '@dukang/shared-types';
|
||||
|
||||
type OssUploadFieldProps = {
|
||||
@@ -61,6 +62,11 @@ export default function OssUploadField({
|
||||
const [profile, setProfile] = useState<PartnerProfile | null>(null);
|
||||
const [clientConfig, setClientConfig] = useState<ClientRuntimeConfig | null>(null);
|
||||
|
||||
function showUploadError(text: string) {
|
||||
setError(text);
|
||||
toastError(text);
|
||||
}
|
||||
|
||||
const resolvedAccept =
|
||||
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? 'image/*,.pdf' : 'image/*');
|
||||
const inWechat = isWechatEnv();
|
||||
@@ -91,7 +97,7 @@ export default function OssUploadField({
|
||||
async function uploadSelectedFile(file: File) {
|
||||
if (file.size > DEFAULT_MAX_MB * 1024 * 1024) {
|
||||
const text = `文件不能超过 ${DEFAULT_MAX_MB}MB`;
|
||||
setError(text);
|
||||
showUploadError(text);
|
||||
return;
|
||||
}
|
||||
setUploading(true);
|
||||
@@ -100,7 +106,7 @@ export default function OssUploadField({
|
||||
const result = await enqueueUpload(() => uploadFileToOss(file, { bizType, mediaType }));
|
||||
onChange?.(result.url);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '上传失败');
|
||||
showUploadError(e instanceof Error ? e.message : '上传失败');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (inputRef.current) inputRef.current.value = '';
|
||||
@@ -114,7 +120,7 @@ export default function OssUploadField({
|
||||
await authorizePartnerWechat();
|
||||
} catch (e) {
|
||||
const text = e instanceof Error ? e.message : '微信授权失败';
|
||||
setError(text);
|
||||
showUploadError(text);
|
||||
setAuthorizing(false);
|
||||
}
|
||||
}
|
||||
@@ -149,7 +155,7 @@ export default function OssUploadField({
|
||||
|
||||
if (useWechatPicker && needsAuth) {
|
||||
const text = '请先完成微信授权后再上传照片';
|
||||
setError(text);
|
||||
showUploadError(text);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,7 +165,7 @@ export default function OssUploadField({
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : '无法打开相册';
|
||||
if (/cancel/i.test(msg)) return;
|
||||
setError(formatWechatUploadError(e));
|
||||
showUploadError(formatWechatUploadError(e));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
|
||||
const TABS = [
|
||||
{ to: '/stores/new', icon: 'add_business', label: '录入新店' },
|
||||
{ to: '/stores', end: true, icon: 'store', label: '我的门店' },
|
||||
{ to: '/stores', end: true, icon: 'store', label: '门店管理' },
|
||||
{ to: '/stores/new', icon: 'add_business', label: '录入门店' },
|
||||
{ to: '/me', icon: 'person', label: '我的' },
|
||||
] as const;
|
||||
|
||||
export default function SubAccountLayout() {
|
||||
const { account, logout } = usePartnerSession();
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header app-page-header partner-sub-header">
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<div>
|
||||
<h1 className="app-page-title" style={{ fontSize: 18 }}>{account?.name || '拓店账号'}</h1>
|
||||
<p className="label-md text-muted">{account?.companyName || '门店入驻'}</p>
|
||||
</div>
|
||||
<button type="button" className="partner-menu-icon" onClick={logout} aria-label="退出登录">
|
||||
<span className="material-symbols-outlined">logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<Outlet />
|
||||
<nav className="app-tabbar">
|
||||
{TABS.map((tab) => (
|
||||
|
||||
@@ -21,7 +21,7 @@ export function partnerHomePath(account: PartnerMe | null | undefined): string {
|
||||
return isSubAccount(account) ? '/stores/new?step=1' : '/';
|
||||
}
|
||||
|
||||
export const SUB_ACCOUNT_ALLOWED_PREFIXES = ['/stores', '/login'];
|
||||
export const SUB_ACCOUNT_ALLOWED_PREFIXES = ['/stores', '/me', '/login'];
|
||||
|
||||
export function isSubAccountPath(pathname: string): boolean {
|
||||
if (pathname === '/login') return true;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PARTNER_STAFF_ROLE_LABELS, type PartnerStaffRole } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
|
||||
export default function PartnerMePage() {
|
||||
const { account, refresh, logout } = usePartnerSession();
|
||||
const [name, setName] = useState(account?.name ?? '');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setName(account?.name ?? '');
|
||||
}, [account?.name]);
|
||||
|
||||
const roleLabel = account?.staffRole
|
||||
? PARTNER_STAFF_ROLE_LABELS[account.staffRole as PartnerStaffRole] || account.staffRole
|
||||
: '拓店账号';
|
||||
|
||||
async function handleSave() {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) {
|
||||
toastError('请输入姓名');
|
||||
return;
|
||||
}
|
||||
if (trimmed === account?.name) {
|
||||
toastSuccess('已保存');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await request('PARTNER_H5', '/partner/me', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: trimmed }),
|
||||
});
|
||||
await refresh();
|
||||
toastSuccess('已保存');
|
||||
} catch (e) {
|
||||
toastError(e instanceof Error ? e.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page partner-me-page">
|
||||
<section className="partner-profile-card" style={{ margin: '16px var(--space-page)' }}>
|
||||
<div className="partner-profile-avatar">
|
||||
<span className="material-symbols-outlined">person</span>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h2 className="headline-lg" style={{ fontSize: 20 }}>{account?.name || '我的'}</h2>
|
||||
<p className="label-md text-muted" style={{ marginTop: 4 }}>{roleLabel}</p>
|
||||
{account?.companyName ? (
|
||||
<p className="body-md text-muted" style={{ marginTop: 4 }}>{account.companyName}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="partner-form-section" style={{ padding: '0 var(--space-page)' }}>
|
||||
<label className="partner-form-label" htmlFor="me-name">姓名</label>
|
||||
<input
|
||||
id="me-name"
|
||||
className="partner-form-input"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="请输入姓名"
|
||||
maxLength={32}
|
||||
/>
|
||||
|
||||
<label className="partner-form-label" style={{ marginTop: 16 }}>手机号</label>
|
||||
<input
|
||||
className="partner-form-input"
|
||||
value={account?.phone ?? ''}
|
||||
readOnly
|
||||
disabled
|
||||
/>
|
||||
<p className="label-md text-muted" style={{ marginTop: 4 }}>手机号由管理员维护,如需修改请联系总部</p>
|
||||
</section>
|
||||
|
||||
<div style={{ padding: '24px var(--space-page) 100px' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-primary"
|
||||
style={{ width: '100%' }}
|
||||
disabled={saving}
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
{saving ? '保存中…' : '保存'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-outline"
|
||||
style={{ width: '100%', marginTop: 12 }}
|
||||
onClick={logout}
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { fetchPartnerCities, type OpenCityOption } from '../lib/upload';
|
||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
||||
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
import { isSubAccount } from '../lib/partnerAccess';
|
||||
|
||||
import {
|
||||
|
||||
@@ -79,6 +80,8 @@ export default function StoreCreatePage() {
|
||||
|
||||
const { account, refresh } = usePartnerSession();
|
||||
|
||||
const subAccountLayout = isSubAccount(account);
|
||||
|
||||
const accountId = account?.id;
|
||||
|
||||
const wechatReady = !!account?.hasWechat;
|
||||
@@ -499,7 +502,7 @@ export default function StoreCreatePage() {
|
||||
|
||||
return (
|
||||
|
||||
<div className="partner-page-sticky">
|
||||
<div className={`partner-page-sticky${subAccountLayout ? ' partner-page-sticky--tabbar' : ''}`}>
|
||||
|
||||
<PageHeader title="录入新门店" onBack={() => navigate('/stores')} />
|
||||
|
||||
@@ -915,7 +918,7 @@ export default function StoreCreatePage() {
|
||||
|
||||
|
||||
|
||||
<footer className="partner-sticky-footer">
|
||||
<footer className={`partner-sticky-footer${subAccountLayout ? ' partner-sticky-footer--above-tabbar' : ''}`}>
|
||||
|
||||
{step > 1 && (
|
||||
|
||||
|
||||
@@ -1290,6 +1290,15 @@ nav.app-tabbar .app-tabbar-label {
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.partner-page-sticky--tabbar {
|
||||
padding-bottom: calc(88px + 56px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.partner-sticky-footer--above-tabbar {
|
||||
bottom: calc(56px + env(safe-area-inset-bottom, 0px));
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.partner-stepper {
|
||||
--step-circle-size: 32px;
|
||||
padding: 16px var(--space-page) 0;
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface PartnerLogRowDto {
|
||||
accountName: string | null;
|
||||
accountPhone: string | null;
|
||||
companyName: string | null;
|
||||
isSubAccount?: boolean;
|
||||
staffRole?: string | null;
|
||||
category: PartnerLogCategory | null;
|
||||
eventName: string;
|
||||
clientApp: string | null;
|
||||
|
||||
@@ -12,6 +12,10 @@ export interface PartnerMe {
|
||||
primaryAccountId?: string;
|
||||
}
|
||||
|
||||
export interface UpdatePartnerMeRequest {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface PartnerStaffItem {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -139,10 +139,22 @@ export class AdminHqAccountsService {
|
||||
if (conflict) throw new BadRequestException('用户名已存在');
|
||||
}
|
||||
|
||||
if (dto.phone !== undefined) {
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new BadRequestException('请输入正确的手机号');
|
||||
}
|
||||
const phoneTaken = await this.prisma.hqAccount.findUnique({ where: { phone } });
|
||||
if (phoneTaken && phoneTaken.id !== id) {
|
||||
throw new BadRequestException('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
const account = await this.prisma.hqAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.phone !== undefined ? { phone: dto.phone.trim() } : {}),
|
||||
...(dto.loginName !== undefined ? { loginName: dto.loginName.trim() } : {}),
|
||||
...(dto.password ? { passwordHash: hashPassword(dto.password) } : {}),
|
||||
...(dto.adminRole !== undefined
|
||||
|
||||
@@ -64,23 +64,39 @@ export class AdminPartnerLogsService {
|
||||
return where;
|
||||
}
|
||||
|
||||
private async expandPrimaryWithChildren(primaryIds: bigint[]): Promise<bigint[]> {
|
||||
if (!primaryIds.length) return [];
|
||||
const children = await this.prisma.partnerAccount.findMany({
|
||||
where: { parentAccountId: { in: primaryIds } },
|
||||
select: { id: true },
|
||||
});
|
||||
return [...primaryIds, ...children.map((c) => c.id)];
|
||||
}
|
||||
|
||||
private async resolvePartnerAccountIds(
|
||||
query: AdminPartnerLogsQueryDto,
|
||||
): Promise<bigint[] | undefined> {
|
||||
if (query.partnerAccountId) return [BigInt(query.partnerAccountId)];
|
||||
if (query.partnerId) return [BigInt(query.partnerId)];
|
||||
if (query.partnerId) {
|
||||
return this.expandPrimaryWithChildren([BigInt(query.partnerId)]);
|
||||
}
|
||||
|
||||
const accountWhere: Prisma.PartnerAccountWhereInput = { isPrimary: 1 };
|
||||
if (query.companyName) accountWhere.companyName = { contains: query.companyName };
|
||||
if (query.phone) accountWhere.phone = { contains: query.phone };
|
||||
|
||||
if (query.companyName || query.phone) {
|
||||
if (query.phone) {
|
||||
const accounts = await this.prisma.partnerAccount.findMany({
|
||||
where: accountWhere,
|
||||
where: { phone: { contains: query.phone } },
|
||||
select: { id: true },
|
||||
take: 200,
|
||||
});
|
||||
return accounts.map((a) => a.id);
|
||||
}
|
||||
|
||||
if (query.companyName) {
|
||||
const primaries = await this.prisma.partnerAccount.findMany({
|
||||
where: { isPrimary: 1, companyName: { contains: query.companyName } },
|
||||
select: { id: true },
|
||||
take: 100,
|
||||
});
|
||||
return accounts.map((a) => a.id);
|
||||
return this.expandPrimaryWithChildren(primaries.map((a) => a.id));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -102,7 +118,16 @@ export class AdminPartnerLogsService {
|
||||
const accounts = accountIds.length
|
||||
? await this.prisma.partnerAccount.findMany({
|
||||
where: { id: { in: accountIds } },
|
||||
select: { id: true, name: true, phone: true, companyName: true },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
phone: true,
|
||||
companyName: true,
|
||||
isPrimary: true,
|
||||
parentAccountId: true,
|
||||
staffRole: true,
|
||||
parent: { select: { id: true, companyName: true } },
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
@@ -110,13 +135,19 @@ export class AdminPartnerLogsService {
|
||||
|
||||
return rows.map((row) => {
|
||||
const account = accountMap.get(row.partnerAccountId.toString());
|
||||
const isSubAccount = !!account?.parentAccountId;
|
||||
const primaryId = isSubAccount
|
||||
? account?.parent?.id?.toString() ?? null
|
||||
: account?.id.toString() ?? null;
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
partnerId: row.partnerAccountId.toString(),
|
||||
partnerId: primaryId ?? row.partnerAccountId.toString(),
|
||||
partnerAccountId: row.partnerAccountId.toString(),
|
||||
accountName: account?.name ?? null,
|
||||
accountPhone: account?.phone ?? null,
|
||||
companyName: account?.companyName ?? null,
|
||||
companyName: isSubAccount ? null : account?.companyName ?? null,
|
||||
isSubAccount,
|
||||
staffRole: account?.staffRole ?? null,
|
||||
category: resolvePartnerLogCategory(row.eventName),
|
||||
eventName: row.eventName,
|
||||
clientApp: row.clientApp,
|
||||
|
||||
@@ -252,11 +252,15 @@ export class AdminPartnersService {
|
||||
assertPartnerCommissionRates(city, orderCommissionRate, redeemCommissionRate);
|
||||
}
|
||||
|
||||
const phoneChanged =
|
||||
dto.phone !== undefined && dto.phone.trim() !== existing.phone;
|
||||
|
||||
const account = await this.prisma.partnerAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
|
||||
...(dto.phone !== undefined ? { phone: dto.phone.trim() } : {}),
|
||||
...(phoneChanged ? { wxOpenId: null, wxUnionId: null } : {}),
|
||||
...(dto.companyName !== undefined ? { companyName: dto.companyName.trim() } : {}),
|
||||
...(dto.address !== undefined ? { address: dto.address.trim() } : {}),
|
||||
...(dto.contactPhone !== undefined ? { contactPhone: dto.contactPhone.trim() } : {}),
|
||||
@@ -446,6 +450,10 @@ export class AdminPartnersService {
|
||||
throw new BadRequestException('该手机号已被使用');
|
||||
}
|
||||
data.phone = phone;
|
||||
if (phone !== existing.phone) {
|
||||
data.wxOpenId = null;
|
||||
data.wxUnionId = null;
|
||||
}
|
||||
}
|
||||
|
||||
const account = await this.prisma.partnerAccount.update({ where: { id }, data });
|
||||
|
||||
@@ -734,6 +734,10 @@ export class UpdateHqAccountDto {
|
||||
@IsString()
|
||||
loginName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { BadRequestException, Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { SettlementService } from './settlement.service';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
||||
@@ -9,6 +10,12 @@ import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
|
||||
class UpdatePartnerMeDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
}
|
||||
|
||||
@Controller('partner/settlement')
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
export class SettlementController {
|
||||
@@ -156,8 +163,26 @@ export class PartnerMeController {
|
||||
|
||||
@Get()
|
||||
async me(@CurrentUser() user: AuthUser) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
return this.buildPartnerMe(user.actorId);
|
||||
}
|
||||
|
||||
@Put()
|
||||
async update(@CurrentUser() user: AuthUser, @Body() dto: UpdatePartnerMeDto) {
|
||||
if (dto.name !== undefined && !dto.name.trim()) {
|
||||
throw new BadRequestException('姓名不能为空');
|
||||
}
|
||||
if (dto.name !== undefined) {
|
||||
await this.prisma.partnerAccount.update({
|
||||
where: { id: user.actorId },
|
||||
data: { name: dto.name.trim() },
|
||||
});
|
||||
}
|
||||
return this.buildPartnerMe(user.actorId);
|
||||
}
|
||||
|
||||
private async buildPartnerMe(actorId: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: actorId },
|
||||
});
|
||||
let primary = account;
|
||||
if (account.isPrimary !== 1 && account.parentAccountId) {
|
||||
|
||||
Reference in New Issue
Block a user