fix: validate signatures and restore report metrics
This commit is contained in:
@@ -923,6 +923,19 @@ export type ReportTask = DictionaryItem & {
|
||||
signature?: { id: string; name: string; purpose?: string | null; drainageInfo?: Record<string, unknown> | null };
|
||||
drainageInfo?: SmsDrainageInfo | null;
|
||||
channel?: { id: string; name: string; code: string };
|
||||
deliveryStats?: {
|
||||
total: number;
|
||||
acceptedCount: number;
|
||||
submitFailureCount: number;
|
||||
submitFailureRate: number;
|
||||
successCount: number;
|
||||
successRate: number;
|
||||
unknownCount: number;
|
||||
unknownRate: number;
|
||||
failureCount: number;
|
||||
failureRate: number;
|
||||
};
|
||||
lastSuccessfulSentAt?: string | null;
|
||||
};
|
||||
|
||||
export type ReportRecord = DictionaryItem & {
|
||||
|
||||
@@ -46,7 +46,26 @@ function ReportStatus({ value }: { value?: string }) {
|
||||
return <Tag tone={meta.tone}>{meta.label}</Tag>;
|
||||
}
|
||||
|
||||
function DetailModal({ drainage, signature, task, onClose }: { drainage?: DrainageItem; signature?: ClientSmsSignature; task: ReportTask; onClose: () => void }) {
|
||||
function DeliveryStats({ task }: { task: ReportTask }) {
|
||||
const stats = task.deliveryStats ?? {
|
||||
submitFailureCount: 0,
|
||||
submitFailureRate: 0,
|
||||
successCount: 0,
|
||||
successRate: 0,
|
||||
unknownCount: 0,
|
||||
unknownRate: 0,
|
||||
failureCount: 0,
|
||||
failureRate: 0,
|
||||
};
|
||||
return <div className="channel-report-stats">
|
||||
<span>成功<strong className="is-success">{stats.successRate}%</strong><b>{stats.successCount.toLocaleString('zh-CN')}</b></span>
|
||||
<span>未知<strong className="is-warning">{stats.unknownRate}%</strong><b>{stats.unknownCount.toLocaleString('zh-CN')}</b></span>
|
||||
<span>回执失败<strong className="is-danger">{stats.failureRate}%</strong><b>{stats.failureCount.toLocaleString('zh-CN')}</b></span>
|
||||
<span>提交失败<strong className="is-danger">{stats.submitFailureRate}%</strong><b>{stats.submitFailureCount.toLocaleString('zh-CN')}</b></span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function DetailModal({ drainage, reportedAt, signature, task, onClose }: { drainage?: DrainageItem; reportedAt?: string | null; signature?: ClientSmsSignature; task: ReportTask; onClose: () => void }) {
|
||||
const payload = asRecord(signature?.drainageInfo);
|
||||
const profile = asRecord(payload.signatureProfile);
|
||||
const reportValues = asRecord(drainage ? drainage.reportValues : payload.signatureReportValues);
|
||||
@@ -56,9 +75,13 @@ function DetailModal({ drainage, signature, task, onClose }: { drainage?: Draina
|
||||
<strong>{drainage ? String(drainage.siteName || drainage.url || '引流信息') : formatSignatureName(signature?.name ?? task.signature?.name)}</strong>
|
||||
<p><span>企业</span><span>{signature?.tenant?.name ?? task.tenantId}</span></p>
|
||||
<p><span>企业应用</span><span>{signature?.application?.name ?? '-'}</span></p>
|
||||
<p><span>提交报备时间</span><DateTime value={drainage?.submittedAt ?? task.createdAt} /></p>
|
||||
<p><span>报备成功时间</span><DateTime value={reportedAt} /></p>
|
||||
<p><span>上次发送成功时间</span><DateTime value={task.lastSuccessfulSentAt} /></p>
|
||||
{!drainage ? <><p><span>签名依据</span><span>{String(profile.basis ?? '-')}</span></p><p><span>公司名称</span><span>{String(profile.companyName ?? '-')}</span></p><p><span>统一社会信用代码</span><span>{String(profile.creditCode ?? '-')}</span></p></> : null}
|
||||
{drainage ? <><p><span>引流地址</span><span>{String(drainage.url ?? '-')}</span></p><p><span>备注</span><span>{String(drainage.remark ?? '-')}</span></p></> : null}
|
||||
{Object.entries(reportValues).map(([key, value]) => <p key={key}><span>{key}</span><span>{typeof value === 'object' ? String(asRecord(value).fileName ?? asRecord(value).fileObjectId ?? '-') : String(value ?? '-')}</span></p>)}
|
||||
<section><h3>今日发送</h3><DeliveryStats task={task} /></section>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
@@ -75,7 +98,7 @@ export function AdminChannelReportPage() {
|
||||
const [libraryFields, setLibraryFields] = useState<DictionaryItem[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [status, setStatus] = useState('all');
|
||||
const [detail, setDetail] = useState<{ task: ReportTask; signature?: ClientSmsSignature; drainage?: DrainageItem }>();
|
||||
const [detail, setDetail] = useState<{ task: ReportTask; reportedAt?: string | null; signature?: ClientSmsSignature; drainage?: DrainageItem }>();
|
||||
const [statusTask, setStatusTask] = useState<ReportTask>();
|
||||
const [nextStatus, setNextStatus] = useState('approved');
|
||||
const [statusReason, setStatusReason] = useState('');
|
||||
@@ -111,8 +134,8 @@ export function AdminChannelReportPage() {
|
||||
return matchesKeyword && (status === 'all' || task.status === status);
|
||||
}), [keyword, signatureMap, status, tasks]);
|
||||
|
||||
function lastRecord(taskId: string, action: string) {
|
||||
return records.find((record) => record.taskId === taskId && record.action === action);
|
||||
function approvedRecord(taskId: string) {
|
||||
return records.find((record) => record.taskId === taskId && record.statusAfter === 'approved');
|
||||
}
|
||||
|
||||
async function saveFieldMapping(nextFields: Parameters<typeof adminApi.replaceChannelReportFields>[2]) {
|
||||
@@ -154,21 +177,20 @@ export function AdminChannelReportPage() {
|
||||
</div>
|
||||
|
||||
<div className="surface channel-report-table">
|
||||
<div className="channel-report-table__head"><span /><span>短信签名 / 引流信息</span><span>报备状态</span><span>提交时间</span><span>报备时间</span><span>最后更新</span><span>发送统计</span><span>操作</span></div>
|
||||
<div className="channel-report-table__head"><span /><span>短信签名 / 引流信息</span><span>报备状态</span><span>提交报备时间</span><span>报备成功时间</span><span>上次发送成功时间</span><span>今日发送</span><span>操作</span></div>
|
||||
{visibleTasks.length === 0 ? <div className="channel-report-empty">当前通道暂无真实报备任务</div> : visibleTasks.map((task) => {
|
||||
const signature = signatureMap.get(task.signatureId);
|
||||
const drainage = task.reportType === 'drainage' ? drainageItems(signature).find((item) => String(item.id) === task.drainageItemId) : undefined;
|
||||
const exported = lastRecord(task.id, 'export');
|
||||
const imported = lastRecord(task.id, 'receipt_import');
|
||||
const reportedAt = approvedRecord(task.id)?.createdAt;
|
||||
return <div className={`channel-report-row ${drainage ? 'channel-report-row--drainage' : 'channel-report-row--signature'}`} key={task.id}>
|
||||
<span />
|
||||
<div className={`channel-report-name ${drainage ? 'channel-report-name--flow' : ''}`}>{drainage ? <i /> : null}<span><strong>{drainage ? String(drainage.siteName || drainage.url || '引流信息') : formatSignatureName(signature?.name ?? task.signature?.name)}</strong><small>{drainage ? `${String(drainage.url ?? '')} · ${formatSignatureName(signature?.name ?? task.signature?.name)}` : signature?.tenant?.name ?? task.tenantId}</small></span></div>
|
||||
<ReportStatus value={task.status} />
|
||||
<DateTime value={drainage?.submittedAt ?? task.createdAt} />
|
||||
<DateTime value={imported?.createdAt ?? exported?.createdAt} />
|
||||
<DateTime value={task.updatedAt} />
|
||||
<div className="channel-report-stats"><span>成功<strong className="is-success">-</strong><b>暂无统计</b></span><span>未知<strong className="is-warning">-</strong><b>暂无统计</b></span><span>失败<strong className="is-danger">-</strong><b>暂无统计</b></span></div>
|
||||
<div className="channel-report-actions"><button onClick={() => setDetail({ task, signature, drainage })} type="button"><Eye size={16} />查看详情</button><button className="is-warning" onClick={() => { setStatusTask(task); setNextStatus(task.status); }} type="button">修改状态</button></div>
|
||||
<DateTime value={reportedAt} />
|
||||
<DateTime value={task.lastSuccessfulSentAt} />
|
||||
<DeliveryStats task={task} />
|
||||
<div className="channel-report-actions"><button onClick={() => setDetail({ task, reportedAt, signature, drainage })} type="button"><Eye size={16} />查看详情</button><button className="is-warning" onClick={() => { setStatusTask(task); setNextStatus(task.status); }} type="button">修改状态</button></div>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { adminApi, type ApplicationReportField, type ClientSmsApplication, type
|
||||
import { Breadcrumb, Button, DeleteRiskAction, FileActions, Input, Modal, Pagination, Select, Tabs, Tag, Textarea } from '@/components/ui';
|
||||
import { displayFileName } from '@/utils/fileName';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
import { isCompleteSmsSignature } from '@/utils/smsSignature';
|
||||
import { getSmsSignatureValidationError, hasForbiddenSmsSignatureCharacter, isCompleteSmsSignature, SMS_SIGNATURE_CHARACTER_ERROR } from '@/utils/smsSignature';
|
||||
|
||||
type CarrierStatus = 'approved' | 'pending' | 'rejected' | 'filing';
|
||||
|
||||
@@ -310,6 +310,7 @@ function SignatureFormModal({
|
||||
reportValues: payload?.signatureReportValues ?? {},
|
||||
});
|
||||
const [reportFields, setReportFields] = useState<ApplicationReportField[]>([]);
|
||||
const [nameInputError, setNameInputError] = useState('');
|
||||
const tenantApplications = applications.filter((application) => application.tenantId === form.tenantId && application.status !== 'deleted');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -327,12 +328,15 @@ function SignatureFormModal({
|
||||
setForm((current) => ({ ...current, reportValues: { ...current.reportValues, [code]: value } }));
|
||||
}
|
||||
|
||||
const signatureNameValid = !nameInputError && isCompleteSmsSignature(form.name);
|
||||
const signatureNameError = nameInputError || (form.name ? getSmsSignatureValidationError(form.name) : undefined);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={onClose} variant="ghost">取消</Button>
|
||||
<Button disabled={!form.tenantId || !isCompleteSmsSignature(form.name) || hasMissingRequiredReportValue(reportFields, form.reportValues)} onClick={() => onSubmit(form)}>保存</Button>
|
||||
<Button disabled={!form.tenantId || !signatureNameValid || hasMissingRequiredReportValue(reportFields, form.reportValues)} onClick={() => onSubmit(form)}>保存</Button>
|
||||
</>
|
||||
)}
|
||||
onClose={onClose}
|
||||
@@ -374,10 +378,18 @@ function SignatureFormModal({
|
||||
value={form.applicationId}
|
||||
/>
|
||||
<Input
|
||||
error={form.name.trim() && !isCompleteSmsSignature(form.name) ? '必须填写完整中文黑括号签名,例如:【某某科技】' : undefined}
|
||||
hint="新增和编辑时都必须保留完整的【】"
|
||||
error={signatureNameError}
|
||||
hint="新增和编辑时必须保留完整的【】,且不能包含空格或不可见字符"
|
||||
label="短信签名"
|
||||
onChange={(event) => update('name', event.target.value)}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
if (hasForbiddenSmsSignatureCharacter(value)) {
|
||||
setNameInputError(SMS_SIGNATURE_CHARACTER_ERROR);
|
||||
return;
|
||||
}
|
||||
setNameInputError('');
|
||||
update('name', value);
|
||||
}}
|
||||
placeholder="请输入完整签名,例如:【某某科技】"
|
||||
required
|
||||
value={form.name}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AlertTriangle, CheckCircle2, Download, FileSpreadsheet, Layers3, Refres
|
||||
import { adminApi, fileDownloadUrl, type ReportMaterialBatchPreflight, type ReportMaterialBatchResult, type ReportMaterialPendingItem } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Tag } from '@/components/ui';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
import { createUuid } from '@/utils/randomId';
|
||||
import { ReportMaterialImportModal } from './ReportMaterialImportModal';
|
||||
|
||||
type Batch = Record<string, unknown> & { id: string; batchNo?: string; status?: string; createdAt?: string; selectedCount?: number; channelCount?: number; exportFiles?: Array<Record<string, unknown>> };
|
||||
@@ -49,7 +50,7 @@ export function AdminReportMaterialsPage() {
|
||||
const chosen = items.filter((item) => selected.has(item.id));
|
||||
if (!chosen.length) { setError('请先选择具备报备资格的资料'); return; }
|
||||
setConfirmOpen(true); setPreflightBusy(true); setPreflight(null); setBatchResult(null); setError(''); setMessage('');
|
||||
setOperationKey(`report-batch:${crypto.randomUUID()}`);
|
||||
setOperationKey(`report-batch:${createUuid()}`);
|
||||
try { setPreflight(await adminApi.preflightReportMaterialBatch({ items: chosen.map(toBatchItem) })); }
|
||||
catch (failure) { setError(failure instanceof Error ? failure.message : '报备资格预检失败'); }
|
||||
finally { setPreflightBusy(false); }
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ArrowLeft, Globe2, Info, RadioTower, RefreshCw } from 'lucide-react';
|
||||
import { adminApi, type ChannelGroup, type DictionaryItem, type EnterpriseApplication, type HttpApiConfig } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Select, Tag } from '@/components/ui';
|
||||
import { isValidMoneyInput, moneyUnitsToYuan, yuanToMoneyUnits } from '@/utils/currency';
|
||||
import { createRandomHex } from '@/utils/randomId';
|
||||
|
||||
type Carrier = 'mobile' | 'unicom' | 'telecom';
|
||||
type QueuePriority = 'normal' | 'priority';
|
||||
@@ -489,8 +490,5 @@ function parseIpAllowlist(value: string) {
|
||||
}
|
||||
|
||||
function generateApplicationPassword() {
|
||||
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
||||
return crypto.randomUUID().replace(/-/g, '').slice(0, 16);
|
||||
}
|
||||
return Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||
return createRandomHex(16);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type ClientSmsSignatureView,
|
||||
type FileRef,
|
||||
} from '@/api/adminApi';
|
||||
import { isCompleteSmsSignature } from '@/utils/smsSignature';
|
||||
import { getSmsSignatureValidationError, hasForbiddenSmsSignatureCharacter, isCompleteSmsSignature, SMS_SIGNATURE_CHARACTER_ERROR } from '@/utils/smsSignature';
|
||||
|
||||
const EMPTY_WORKSPACE: ClientSignatureWorkspace = {
|
||||
items: [],
|
||||
@@ -98,6 +98,7 @@ function SignatureModal({
|
||||
const [uploadingCode, setUploadingCode] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [nameInputError, setNameInputError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const request = applicationId
|
||||
@@ -122,7 +123,7 @@ function SignatureModal({
|
||||
|
||||
async function save() {
|
||||
if (!isCompleteSmsSignature(name)) {
|
||||
setError('短信签名必须包含完整中文黑括号,例如:【某某科技】');
|
||||
setError(getSmsSignatureValidationError(name) ?? '短信签名格式不正确');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
@@ -143,7 +144,8 @@ function SignatureModal({
|
||||
}
|
||||
|
||||
const missingRequired = fields.some((field) => field.required && !values[field.code]);
|
||||
const signatureNameValid = isCompleteSmsSignature(name);
|
||||
const signatureNameValid = !nameInputError && isCompleteSmsSignature(name);
|
||||
const signatureNameError = nameInputError || (name ? getSmsSignatureValidationError(name) : undefined);
|
||||
return <Modal
|
||||
footer={<><Button onClick={onClose} variant="ghost">取消</Button><Button disabled={!signatureNameValid || missingRequired || saving || Boolean(uploadingCode)} onClick={() => void save()}>{saving ? '提交中...' : '提交审核'}</Button></>}
|
||||
onClose={onClose}
|
||||
@@ -160,10 +162,18 @@ function SignatureModal({
|
||||
value={applicationId}
|
||||
/>
|
||||
<Input
|
||||
error={name.trim() && !signatureNameValid ? '必须填写完整中文黑括号签名,例如:【某某科技】' : undefined}
|
||||
hint="新增和编辑时都必须保留完整的【】"
|
||||
error={signatureNameError}
|
||||
hint="新增和编辑时必须保留完整的【】,且不能包含空格或不可见字符"
|
||||
label="短信签名"
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
if (hasForbiddenSmsSignatureCharacter(value)) {
|
||||
setNameInputError(SMS_SIGNATURE_CHARACTER_ERROR);
|
||||
return;
|
||||
}
|
||||
setNameInputError('');
|
||||
setName(value);
|
||||
}}
|
||||
placeholder="请输入完整签名,例如:【某某科技】"
|
||||
required
|
||||
value={name}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { AlertTriangle, ShieldCheck, Trash2 } from 'lucide-react';
|
||||
import { adminApi, clientApi, type DeletionPreflight, type DeletionResult, type DeletionTargetType } from '@/api/adminApi';
|
||||
import { createUuid } from '@/utils/randomId';
|
||||
import { Button } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
import { Textarea } from './Textarea';
|
||||
@@ -24,7 +25,7 @@ export function DeleteRiskAction({ portal, targetType, targetId, children = '删
|
||||
const [idempotencyKey, setIdempotencyKey] = useState('');
|
||||
|
||||
async function begin() {
|
||||
const key = `delete:${targetType}:${targetId}:${crypto.randomUUID()}`;
|
||||
const key = `delete:${targetType}:${targetId}:${createUuid()}`;
|
||||
setOpen(true); setLoading(true); setPreflight(null); setResult(null); setReason(''); setError(''); setIdempotencyKey(key);
|
||||
try {
|
||||
const data = portal === 'admin'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { adminApi, type ManualRechargePreflight, type ManualRechargeResult } from '@/api/adminApi';
|
||||
import { formatCents, isValidMoneyInput, yuanToMoneyUnits } from '@/utils/currency';
|
||||
import { createUuid } from '@/utils/randomId';
|
||||
import { Button } from './Button';
|
||||
import { Input } from './Input';
|
||||
import { Modal } from './Modal';
|
||||
@@ -73,7 +74,7 @@ export function ManualRechargeDialog({ initialTargetId, lockTarget = false, onCl
|
||||
try {
|
||||
const preview = await adminApi.preflightManualRecharge({ tenantId, amountCents: yuanToMoneyUnits(amount) });
|
||||
setReview(preview);
|
||||
setIdempotencyKey(`manual-recharge:${crypto.randomUUID()}`);
|
||||
setIdempotencyKey(`manual-recharge:${createUuid()}`);
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '人工充值资格核对失败');
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { AlertTriangle, Check, ShieldCheck } from 'lucide-react';
|
||||
import { adminApi, type ReviewDecisionResult, type ReviewPreflight } from '@/api/adminApi';
|
||||
import { createUuid } from '@/utils/randomId';
|
||||
import { Button } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
@@ -28,7 +29,7 @@ export function RiskAction({
|
||||
const [idempotencyKey, setIdempotencyKey] = useState('');
|
||||
|
||||
async function begin() {
|
||||
const key = `review:${targetType}:${targetId}:${crypto.randomUUID()}`;
|
||||
const key = `review:${targetType}:${targetId}:${createUuid()}`;
|
||||
setOpen(true);
|
||||
setLoading(true);
|
||||
setResult(null);
|
||||
|
||||
@@ -7182,7 +7182,7 @@ h3 {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
min-width: 260px;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
@@ -11443,11 +11443,12 @@ h3 {
|
||||
}
|
||||
|
||||
.channel-report-row > :nth-child(3)::before { color: var(--color-text-muted); content: "报备状态"; font-size: var(--font-size-xs); }
|
||||
.channel-report-row > :nth-child(4)::before { color: var(--color-text-muted); content: "提交时间"; font-size: var(--font-size-xs); }
|
||||
.channel-report-row > :nth-child(5)::before { color: var(--color-text-muted); content: "报备时间"; font-size: var(--font-size-xs); }
|
||||
.channel-report-row > :nth-child(6)::before { color: var(--color-text-muted); content: "最后更新"; font-size: var(--font-size-xs); }
|
||||
.channel-report-row > :nth-child(4)::before { color: var(--color-text-muted); content: "提交报备时间"; font-size: var(--font-size-xs); }
|
||||
.channel-report-row > :nth-child(5)::before { color: var(--color-text-muted); content: "报备成功时间"; font-size: var(--font-size-xs); }
|
||||
.channel-report-row > :nth-child(6)::before { color: var(--color-text-muted); content: "上次发送成功"; font-size: var(--font-size-xs); }
|
||||
|
||||
.channel-report-stats {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export function createUuid() {
|
||||
const webCrypto = globalThis.crypto;
|
||||
if (typeof webCrypto?.randomUUID === 'function') {
|
||||
return webCrypto.randomUUID();
|
||||
}
|
||||
|
||||
const bytes = createRandomBytes(16);
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, '0'));
|
||||
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`;
|
||||
}
|
||||
|
||||
export function createRandomHex(length: number) {
|
||||
const normalizedLength = Math.max(0, Math.floor(length));
|
||||
return Array.from(createRandomBytes(Math.ceil(normalizedLength / 2)), (value) => value.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
.slice(0, normalizedLength);
|
||||
}
|
||||
|
||||
function createRandomBytes(length: number) {
|
||||
const bytes = new Uint8Array(length);
|
||||
const webCrypto = globalThis.crypto;
|
||||
if (typeof webCrypto?.getRandomValues === 'function') {
|
||||
webCrypto.getRandomValues(bytes);
|
||||
return bytes;
|
||||
}
|
||||
for (let index = 0; index < bytes.length; index += 1) {
|
||||
bytes[index] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
const LEADING_SMS_SIGNATURE = /^【[^】]+】/;
|
||||
const FORBIDDEN_SMS_SIGNATURE_CHARACTER = /[\p{White_Space}\p{Cc}\p{Default_Ignorable_Code_Point}]/u;
|
||||
export const SMS_SIGNATURE_CHARACTER_ERROR = '短信签名不能包含空格、换行或不可见字符';
|
||||
export const SMS_SIGNATURE_FORMAT_ERROR = '必须填写完整中文黑括号签名,例如:【某某科技】';
|
||||
|
||||
export function formatSmsSignature(name?: string | null) {
|
||||
const innerName = (name ?? '').trim().replace(/^[【\[]+|[】\]]+$/g, '').trim();
|
||||
@@ -6,9 +9,23 @@ export function formatSmsSignature(name?: string | null) {
|
||||
}
|
||||
|
||||
export function isCompleteSmsSignature(name?: string | null) {
|
||||
const value = (name ?? '').trim();
|
||||
return getSmsSignatureValidationError(name) === undefined;
|
||||
}
|
||||
|
||||
export function hasForbiddenSmsSignatureCharacter(name?: string | null) {
|
||||
return FORBIDDEN_SMS_SIGNATURE_CHARACTER.test(name ?? '');
|
||||
}
|
||||
|
||||
export function getSmsSignatureValidationError(name?: string | null) {
|
||||
const value = name ?? '';
|
||||
if (!value) {
|
||||
return SMS_SIGNATURE_FORMAT_ERROR;
|
||||
}
|
||||
if (hasForbiddenSmsSignatureCharacter(value)) {
|
||||
return SMS_SIGNATURE_CHARACTER_ERROR;
|
||||
}
|
||||
const match = value.match(/^【([^【】]+)】$/);
|
||||
return Boolean(match && match[1] === match[1].trim());
|
||||
return match ? undefined : SMS_SIGNATURE_FORMAT_ERROR;
|
||||
}
|
||||
|
||||
export function replaceLeadingSmsSignature(content: string, signatureName?: string | null) {
|
||||
|
||||
Reference in New Issue
Block a user