feat: restore credit limits and harden SMS sending
This commit is contained in:
+4
-1
@@ -255,7 +255,7 @@ export type DashboardResponse = {
|
||||
recentFailed: number;
|
||||
alertCount: number;
|
||||
};
|
||||
accounts: Array<{ id: string; tenantId: string; balanceCents: number; status: string; tenant?: TenantOption }>;
|
||||
accounts: Array<{ id: string; tenantId: string; balanceCents: number; creditCents: number; status: string; tenant?: TenantOption }>;
|
||||
recentTasks: Array<Record<string, unknown>>;
|
||||
recentRecharges: Array<RechargeOrder>;
|
||||
};
|
||||
@@ -661,6 +661,7 @@ export type TenantAccount = {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
balanceCents: number;
|
||||
creditCents: number;
|
||||
status: string;
|
||||
tenant?: TenantOption;
|
||||
};
|
||||
@@ -953,6 +954,8 @@ export const adminApi = {
|
||||
listSystemLogs: (query: { tenantId?: string; keyword?: string; level?: string; module?: string; range?: string; page?: number; pageSize?: number }) =>
|
||||
request<OperationLogResponse>(withQuery('/admin/system-logs', query)),
|
||||
listAccounts: () => request<TenantAccount[]>('/admin/billing/accounts'),
|
||||
updateCreditLimit: (tenantId: string, body: { creditCents: number; operatorId?: string; remark?: string }) =>
|
||||
request<TenantAccount>(`/admin/billing/accounts/${tenantId}/credit-limit`, { method: 'POST', body: JSON.stringify(body) }),
|
||||
listManualRecharges: (tenantId?: string) => request<RechargeOrder[]>(withQuery('/admin/billing/manual-recharges', { tenantId })),
|
||||
createManualRecharge: (body: { tenantId: string; amountCents: number; operatorId?: string; remark?: string }) =>
|
||||
request<RechargeOrder>('/admin/billing/manual-recharges', { method: 'POST', body: JSON.stringify(body) }),
|
||||
|
||||
@@ -69,6 +69,7 @@ export function AdminCustomerDetailPage() {
|
||||
<div className="surface mini-status-card"><Server size={22} /><div><span>企业编码</span><strong>{tenant?.code ?? '-'}</strong><small>{tenant?.status ?? '-'}</small></div></div>
|
||||
<div className="surface mini-status-card"><MessageSquare size={22} /><div><span>计费方式</span><strong>按量计费</strong><small>仅从现金余额扣费</small></div></div>
|
||||
<div className="surface mini-status-card"><FileText size={22} /><div><span>现金余额</span><strong>¥{formatCents(account?.balanceCents)}</strong><small>真实账户余额</small></div></div>
|
||||
<div className="surface mini-status-card"><FileText size={22} /><div><span>授信额度</span><strong>¥{formatCents(account?.creditCents)}</strong><small>可配置为正数、负数或 0</small></div></div>
|
||||
</div>
|
||||
|
||||
<div className="surface section-stack">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Breadcrumb, Button, FileActions, Input, Select, Textarea } from '@/comp
|
||||
type EnterpriseForm = {
|
||||
name: string;
|
||||
creditCode: string;
|
||||
creditLimit: string;
|
||||
province: string;
|
||||
city: string;
|
||||
address: string;
|
||||
@@ -44,6 +45,7 @@ const cityOptionsByProvince: Record<string, Array<{ label: string; value: string
|
||||
const emptyForm: EnterpriseForm = {
|
||||
name: '',
|
||||
creditCode: '',
|
||||
creditLimit: '0',
|
||||
province: '',
|
||||
city: '',
|
||||
address: '',
|
||||
@@ -56,11 +58,12 @@ const emptyForm: EnterpriseForm = {
|
||||
photoContentType: '',
|
||||
};
|
||||
|
||||
function formFromTenant(tenant: TenantOption): EnterpriseForm {
|
||||
function formFromTenant(tenant: TenantOption, creditCents = 0): EnterpriseForm {
|
||||
const profile = tenant.enterpriseProfile;
|
||||
return {
|
||||
name: tenant.name,
|
||||
creditCode: profile?.creditCode ?? '',
|
||||
creditLimit: String(creditCents / 100),
|
||||
province: profile?.province ?? '',
|
||||
city: profile?.city ?? '',
|
||||
address: profile?.address ?? '',
|
||||
@@ -89,9 +92,9 @@ export function AdminCustomerFormPage() {
|
||||
setForm(emptyForm);
|
||||
return;
|
||||
}
|
||||
adminApi.getTenant(enterpriseId)
|
||||
.then((tenant) => {
|
||||
setForm(formFromTenant(tenant));
|
||||
Promise.all([adminApi.getTenant(enterpriseId), adminApi.listAccounts()])
|
||||
.then(([tenant, accounts]) => {
|
||||
setForm(formFromTenant(tenant, accounts.find((account) => account.tenantId === enterpriseId)?.creditCents ?? 0));
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '企业信息加载失败'));
|
||||
@@ -117,21 +120,30 @@ export function AdminCustomerFormPage() {
|
||||
if (!form.creditCode.trim()) nextErrors.creditCode = '请填写统一社会信用代码';
|
||||
if (!form.contactName.trim()) nextErrors.contactName = '请填写联系人姓名';
|
||||
if (!form.contactPhone.trim()) nextErrors.contactPhone = '请填写手机号';
|
||||
const creditLimit = Number(form.creditLimit);
|
||||
if (!Number.isFinite(creditLimit)) nextErrors.creditLimit = '请填写有效的授信额度';
|
||||
setErrors(nextErrors);
|
||||
return Object.keys(nextErrors).length === 0;
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
async function submitForm() {
|
||||
if (!validateForm()) return;
|
||||
setSaving(true);
|
||||
const { photoContentType, photoFileName, ...payload } = form;
|
||||
const request = isEdit && enterpriseId
|
||||
? adminApi.updateTenant(enterpriseId, payload)
|
||||
: adminApi.createTenant(payload);
|
||||
request
|
||||
.then(() => navigate('/admin/customers'))
|
||||
.catch((failure: Error) => setError(failure.message || '企业保存失败'))
|
||||
.finally(() => setSaving(false));
|
||||
const { creditLimit, photoContentType, photoFileName, ...payload } = form;
|
||||
try {
|
||||
const tenant = isEdit && enterpriseId
|
||||
? await adminApi.updateTenant(enterpriseId, payload)
|
||||
: await adminApi.createTenant(payload);
|
||||
await adminApi.updateCreditLimit(tenant.id, {
|
||||
creditCents: Math.round(Number(creditLimit) * 100),
|
||||
remark: isEdit ? '企业编辑页调整授信额度' : '创建企业初始化授信额度',
|
||||
});
|
||||
navigate('/admin/customers');
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '企业保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function uploadEnterprisePhoto(file: File | undefined) {
|
||||
@@ -204,6 +216,18 @@ export function AdminCustomerFormPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-grid form-grid--two">
|
||||
<Input
|
||||
error={errors.creditLimit}
|
||||
hint="可填写正数、负数或 0;短信发送时判断余额与授信额度之和是否大于 0。"
|
||||
label="授信额度(元)"
|
||||
onChange={(event) => updateForm('creditLimit', event.target.value)}
|
||||
required
|
||||
type="number"
|
||||
value={form.creditLimit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-grid form-grid--two">
|
||||
<Select label="省/直辖市" onChange={(event) => updateForm('province', event.target.value)} options={provinceOptions} value={form.province} />
|
||||
<Select label="市/区" onChange={(event) => updateForm('city', event.target.value)} options={cityOptions} value={form.city} />
|
||||
|
||||
@@ -86,6 +86,7 @@ export function AdminCustomersPage({ basePath = '/admin/customers' }: AdminCusto
|
||||
);
|
||||
},
|
||||
},
|
||||
{ key: 'creditLimit', title: '授信额度', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.account?.creditCents ?? 0)}` },
|
||||
{ key: 'todaySpend', title: '今日消费', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.todaySpendCents)}` },
|
||||
{ key: 'todayRefund', title: '今日返还', width: '150px', align: 'right', render: (record) => `¥${formatCents(record.todayRefundCents)}` },
|
||||
{ key: 'status', title: '企业状态', width: '130px', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'warning'}>{record.status === 'active' ? '正常' : '已禁用'}</Tag> },
|
||||
|
||||
@@ -69,7 +69,7 @@ export function AdminHome() {
|
||||
const todaySpend = Math.abs(dashboard?.recentRecharges
|
||||
.filter((item) => item.tenantId === account.tenantId)
|
||||
.reduce((sum, item) => sum + item.amountCents, 0) ?? 0) / 100;
|
||||
const availableBalance = account.balanceCents / 100;
|
||||
const availableBalance = (account.balanceCents + account.creditCents) / 100;
|
||||
return {
|
||||
id: account.tenantId,
|
||||
enterprise: account.tenant?.name ?? account.tenantId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
export function ClientBillingPage() {
|
||||
const [balanceCents, setBalanceCents] = useState(0);
|
||||
const [creditCents, setCreditCents] = useState(0);
|
||||
const [orders, setOrders] = useState<RechargeOrder[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -21,6 +22,7 @@ export function ClientBillingPage() {
|
||||
Promise.all([clientApi.getDashboard(), clientApi.listOrders()])
|
||||
.then(([dashboard, nextOrders]) => {
|
||||
setBalanceCents(dashboard.accounts[0]?.balanceCents ?? 0);
|
||||
setCreditCents(dashboard.accounts[0]?.creditCents ?? 0);
|
||||
setOrders(nextOrders);
|
||||
setError('');
|
||||
})
|
||||
@@ -36,7 +38,11 @@ export function ClientBillingPage() {
|
||||
<div className="dashboard-grid enterprise-summary-grid">
|
||||
<div className="surface mini-status-card">
|
||||
<WalletCards size={22} />
|
||||
<div><span>当前可用余额</span><strong>¥{formatCents(balanceCents)}</strong><small>短信发送仅按现金余额校验。</small></div>
|
||||
<div><span>现金余额</span><strong>¥{formatCents(balanceCents)}</strong><small>充值和扣费后的账面余额。</small></div>
|
||||
</div>
|
||||
<div className="surface mini-status-card">
|
||||
<WalletCards size={22} />
|
||||
<div><span>可用发送额度</span><strong>¥{formatCents(balanceCents + creditCents)}</strong><small>大于 0 时可以发送短信。</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface section-stack">
|
||||
|
||||
@@ -50,7 +50,7 @@ export function ClientHome() {
|
||||
}, []);
|
||||
|
||||
const account = dashboard?.accounts[0];
|
||||
const availableBalance = (account?.balanceCents ?? 0) / 100;
|
||||
const availableBalance = ((account?.balanceCents ?? 0) + (account?.creditCents ?? 0)) / 100;
|
||||
const todaySpend = (dashboard?.today.spendCents ?? 0) / 100;
|
||||
const todayRefund = Math.abs((dashboard?.transactions._sum.amountCents ?? 0) < 0 ? 0 : dashboard?.transactions._sum.amountCents ?? 0) / 100;
|
||||
const balanceBaseline = Math.max(availableBalance + todaySpend - todayRefund, availableBalance, 1);
|
||||
@@ -102,9 +102,9 @@ export function ClientHome() {
|
||||
|
||||
<div className="dashboard-grid">
|
||||
<div className="surface metric-card metric-card--featured">
|
||||
<span>账户剩余余额</span>
|
||||
<span>可用发送额度</span>
|
||||
<strong>¥{formatAmount(availableBalance)}</strong>
|
||||
<small>今日消费 ¥{formatAmount(todaySpend)}</small>
|
||||
<small>现金余额 ¥{formatCents(account?.balanceCents)}</small>
|
||||
</div>
|
||||
<div className="surface metric-card">
|
||||
<span>今日发送</span>
|
||||
|
||||
Reference in New Issue
Block a user