feat: restore credit limits and harden SMS sending

This commit is contained in:
hectorzhao
2026-07-14 18:46:35 +08:00
parent 3e3b7a9d1a
commit c1a17699db
23 changed files with 796 additions and 56 deletions
@@ -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">
+37 -13
View File
@@ -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} />
+1
View File
@@ -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> },
+1 -1
View File
@@ -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 -1
View File
@@ -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">
+3 -3
View File
@@ -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>