front/src/api/system/batchEmail.ts
zzh 43209625d0 feat: 优化批量邮件页并支持线上安全密码与手动收件人
重构收件人录入与邮箱配置弹窗,修复账号接口解析;支持脱敏密码复用后直接测试与发送。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 11:59:01 +08:00

181 lines
6.0 KiB
TypeScript

import { axiosInstance } from '@/api/rbac'
import { assertBusinessOk, omsUrl, unwrapData } from '@/api/oms/common'
/** 与桌面版 emailAuto.py 一致:阿里企业邮等 */
export type BatchEmailServerType = 'qiye' | 'qiye_ssl' | 'aliyun' | 'aliyun_ssl' | 'mxhichina'
/** 与后端 BatchEmailSmtpHelper.SecretMask 一致,用于线上已保存密码占位 */
export const SMTP_SECRET_MASK = '******'
export function isSmtpSecretMask(value?: string | null): boolean {
return String(value ?? '').trim() === SMTP_SECRET_MASK
}
export interface BatchEmailTemplate {
name: string
subject: string
content: string
}
export interface BatchEmailAccount {
smtpUsername: string
smtpPassword: string
serverType: BatchEmailServerType
fromName?: string
}
export interface BatchEmailSendOneBody {
smtpUsername: string
smtpPassword: string
serverType?: BatchEmailServerType
to: string
subject: string
body: string
bodyType?: 'plain' | 'html'
fromName?: string
}
function pickStr(row: Record<string, unknown>, ...keys: string[]) {
for (const key of keys) {
const val = row[key]
if (val != null && String(val).trim() !== '') return String(val).trim()
}
return ''
}
function normalizeTemplate(raw: unknown): BatchEmailTemplate | null {
if (raw == null || typeof raw !== 'object') return null
const row = raw as Record<string, unknown>
const name = pickStr(row, 'name', 'Name')
if (!name) return null
return {
name,
subject: pickStr(row, 'subject', 'Subject') || name,
content: pickStr(row, 'content', 'Content'),
}
}
function normalizeAccount(raw: unknown): BatchEmailAccount | null {
if (raw == null || typeof raw !== 'object') return null
const row = raw as Record<string, unknown>
const smtpUsername = pickStr(row, 'smtpUsername', 'SmtpUsername')
if (!smtpUsername) return null
const serverType = (pickStr(row, 'serverType', 'ServerType') || 'qiye') as BatchEmailServerType
return {
smtpUsername,
smtpPassword: pickStr(row, 'smtpPassword', 'SmtpPassword'),
serverType,
fromName: pickStr(row, 'fromName', 'FromName') || undefined,
}
}
export interface BatchEmailSendRecord {
id: number
toEmail: string
subject: string
content: string
sendTime: string
sender: string
}
function normalizeRecord(raw: unknown): BatchEmailSendRecord | null {
if (raw == null || typeof raw !== 'object') return null
const row = raw as Record<string, unknown>
const id = Number(row.id ?? row.Id)
if (!Number.isFinite(id)) return null
return {
id,
toEmail: pickStr(row, 'toEmail', 'ToEmail'),
subject: pickStr(row, 'subject', 'Subject'),
content: pickStr(row, 'content', 'Content'),
sendTime: pickStr(row, 'sendTime', 'SendTime'),
sender: pickStr(row, 'sender', 'Sender'),
}
}
function unwrapPageResult(resBody: unknown): { items: unknown[]; total: number } {
assertBusinessOk(resBody)
const root = (resBody as Record<string, unknown>) ?? {}
const inner = (root.data ?? root.Data ?? root) as Record<string, unknown>
const items = inner.items ?? inner.Items ?? inner.list ?? inner.List ?? []
const total = Number(inner.total ?? inner.Total ?? inner.totalCount ?? inner.TotalCount ?? 0)
return {
items: Array.isArray(items) ? items : [],
total: Number.isFinite(total) ? total : 0,
}
}
function unwrapList(resBody: unknown): unknown[] {
assertBusinessOk(resBody)
const root = (resBody as Record<string, unknown>) ?? {}
const inner = root.data ?? root.Data ?? root
if (Array.isArray(inner)) return inner
return unwrapPageResult(resBody).items
}
/** GET /system/batch-email/account */
export async function getBatchEmailAccount(): Promise<BatchEmailAccount | null> {
const res = await axiosInstance.get(omsUrl('/system/batch-email/account'))
const raw = unwrapData<Record<string, unknown> | null>(res.data)
return raw ? normalizeAccount(raw) : null
}
/** GET /system/batch-email/templates/list */
export async function listBatchEmailTemplates(): Promise<BatchEmailTemplate[]> {
const res = await axiosInstance.get(omsUrl('/system/batch-email/templates/list'))
return unwrapList(res.data)
.map(normalizeTemplate)
.filter((item): item is BatchEmailTemplate => item != null)
}
/** POST /system/batch-email/templates/save */
export async function saveBatchEmailTemplate(body: {
name: string
subject: string
content: string
}) {
const res = await axiosInstance.post(omsUrl('/system/batch-email/templates/save'), body)
assertBusinessOk(res.data)
}
/** POST /system/batch-email/templates/delete */
export async function deleteBatchEmailTemplate(body: { name: string }) {
const res = await axiosInstance.post(omsUrl('/system/batch-email/templates/delete'), body)
assertBusinessOk(res.data)
}
/** POST /system/batch-email/account/save */
export async function saveBatchEmailAccount(body: BatchEmailAccount) {
const res = await axiosInstance.post(omsUrl('/system/batch-email/account/save'), body)
assertBusinessOk(res.data)
}
/** POST /system/batch-email/test-connection — 验证 SMTP 账号 */
export async function testBatchEmailConnection(body: {
smtpUsername: string
smtpPassword: string
serverType?: BatchEmailServerType
fromName?: string
}) {
const res = await axiosInstance.post(omsUrl('/system/batch-email/test-connection'), body)
assertBusinessOk(res.data)
}
/** POST /system/batch-email/send-one — 发送单封(批量时由前端循环调用,便于逐条写日志) */
export async function sendBatchEmailOne(body: BatchEmailSendOneBody) {
const res = await axiosInstance.post(omsUrl('/system/batch-email/send-one'), body)
assertBusinessOk(res.data)
}
/** GET /system/batch-email/records/list */
export async function listBatchEmailRecords(params?: { page?: number; pageSize?: number }) {
const res = await axiosInstance.get(omsUrl('/system/batch-email/records/list'), { params })
const page = unwrapPageResult(res.data)
return {
items: page.items
.map(normalizeRecord)
.filter((item): item is BatchEmailSendRecord => item != null),
total: page.total,
}
}