Enables clone-and-deploy with customer CC profiles, message-type dict, improved packing/work-order parse, and related settings UI. Co-authored-by: Cursor <cursoragent@cursor.com>main
parent
627d1b7be8
commit
02ad9d83ff
Binary file not shown.
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 将历史已成功导入的工单/DO/转仓指令补写入 /logs 导入日志。
|
||||||
|
* Usage: pnpm exec tsx scripts/backfill-instruction-import-logs.ts
|
||||||
|
* pnpm exec tsx scripts/backfill-instruction-import-logs.ts --dry-run
|
||||||
|
*/
|
||||||
|
import { backfillInstructionImportLogs } from "../src/services/import/instruction-import-log";
|
||||||
|
import { prisma } from "../src/services/db";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const dryRun = process.argv.includes("--dry-run");
|
||||||
|
const result = await backfillInstructionImportLogs({ dryRun });
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
dryRun,
|
||||||
|
scannedMails: result.scannedMails,
|
||||||
|
created: result.created,
|
||||||
|
skipped: result.skipped,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exitCode = 1;
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { prisma } from "../src/services/db";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const id = BigInt(process.argv[2] || "258");
|
||||||
|
const mail = await prisma.mailMessage.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { attachments: true, parseResult: true },
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
mail,
|
||||||
|
(_k, v) => (typeof v === "bigint" ? Number(v) : v),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { prisma } from "../src/services/db";
|
||||||
|
import { ParsePipeline } from "../src/services/parse/pipeline";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const id = BigInt(process.argv[2] || "258");
|
||||||
|
await prisma.mailMessage.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status: "PARSING", version: { increment: 1 } },
|
||||||
|
});
|
||||||
|
await ParsePipeline.run(id);
|
||||||
|
const mail = await prisma.mailMessage.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { parseResult: true },
|
||||||
|
});
|
||||||
|
const shipments = (mail?.parseResult?.shipments as Array<{ row_status?: string }>) || [];
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
id: Number(id),
|
||||||
|
status: mail?.status,
|
||||||
|
mailType: mail?.mailType,
|
||||||
|
lastError: mail?.lastError,
|
||||||
|
validRows: shipments.filter((s) => s.row_status === "VALID").length,
|
||||||
|
totalRows: shipments.length,
|
||||||
|
header: mail?.parseResult?.containerHeader,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { fail, ok, parseBigIntId, requireAdmin } from "@/lib/api";
|
||||||
|
import { writeAudit } from "@/services/audit";
|
||||||
|
import {
|
||||||
|
deleteCcMessageTypeDict,
|
||||||
|
updateCcMessageTypeDict,
|
||||||
|
} from "@/services/cc/message-type-dict";
|
||||||
|
|
||||||
|
const PatchSchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(64).optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
sort_order: z.number().int().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type RouteContext = { params: Promise<{ id: string }> };
|
||||||
|
|
||||||
|
export async function PATCH(req: Request, ctx: RouteContext) {
|
||||||
|
const guard = await requireAdmin();
|
||||||
|
if (guard.response) return guard.response;
|
||||||
|
|
||||||
|
const { id } = await ctx.params;
|
||||||
|
const rowId = parseBigIntId(id);
|
||||||
|
if (!rowId) return fail("VALIDATION", "Invalid id", 400);
|
||||||
|
|
||||||
|
let body: unknown;
|
||||||
|
try {
|
||||||
|
body = await req.json();
|
||||||
|
} catch {
|
||||||
|
return fail("VALIDATION", "Invalid JSON", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = PatchSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return fail("VALIDATION", parsed.error.issues[0]?.message ?? "Invalid", 400);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parsed.data.name === undefined &&
|
||||||
|
parsed.data.enabled === undefined &&
|
||||||
|
parsed.data.sort_order === undefined
|
||||||
|
) {
|
||||||
|
return fail("VALIDATION", "无变更字段", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const row = await updateCcMessageTypeDict(rowId, {
|
||||||
|
name: parsed.data.name,
|
||||||
|
enabled: parsed.data.enabled,
|
||||||
|
sortOrder: parsed.data.sort_order,
|
||||||
|
});
|
||||||
|
if (!row) return fail("NOT_FOUND", "留言类型不存在", 404);
|
||||||
|
|
||||||
|
await writeAudit({
|
||||||
|
actor: guard.session.username!,
|
||||||
|
action: "CC_MESSAGE_TYPE_UPDATE",
|
||||||
|
payload: {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
enabled: row.enabled,
|
||||||
|
sort_order: row.sort_order,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return ok(row);
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err instanceof Prisma.PrismaClientKnownRequestError &&
|
||||||
|
err.code === "P2002"
|
||||||
|
) {
|
||||||
|
return fail("CONFLICT", "留言类型名称已存在", 409);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(_req: Request, ctx: RouteContext) {
|
||||||
|
const guard = await requireAdmin();
|
||||||
|
if (guard.response) return guard.response;
|
||||||
|
|
||||||
|
const { id } = await ctx.params;
|
||||||
|
const rowId = parseBigIntId(id);
|
||||||
|
if (!rowId) return fail("VALIDATION", "Invalid id", 400);
|
||||||
|
|
||||||
|
const deleted = await deleteCcMessageTypeDict(rowId);
|
||||||
|
if (!deleted) return fail("NOT_FOUND", "留言类型不存在", 404);
|
||||||
|
|
||||||
|
await writeAudit({
|
||||||
|
actor: guard.session.username!,
|
||||||
|
action: "CC_MESSAGE_TYPE_DELETE",
|
||||||
|
payload: { id },
|
||||||
|
});
|
||||||
|
return ok({ id, deleted: true });
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { fail, ok, requireAdmin, requireSession } from "@/lib/api";
|
||||||
|
import { writeAudit } from "@/services/audit";
|
||||||
|
import {
|
||||||
|
createCcMessageTypeDict,
|
||||||
|
listCcMessageTypeDict,
|
||||||
|
} from "@/services/cc/message-type-dict";
|
||||||
|
|
||||||
|
const CreateSchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(64),
|
||||||
|
enabled: z.boolean().optional().default(true),
|
||||||
|
sort_order: z.number().int().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function GET(req: Request) {
|
||||||
|
const guard = await requireSession();
|
||||||
|
if (guard.response) return guard.response;
|
||||||
|
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const enabledOnly = url.searchParams.get("enabled_only") === "1";
|
||||||
|
// 非管理员只能看已启用项(表单下拉)
|
||||||
|
const forForm =
|
||||||
|
enabledOnly || guard.session.role !== "admin";
|
||||||
|
|
||||||
|
const items = await listCcMessageTypeDict({ enabledOnly: forForm });
|
||||||
|
return ok({ items });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const guard = await requireAdmin();
|
||||||
|
if (guard.response) return guard.response;
|
||||||
|
|
||||||
|
let body: unknown;
|
||||||
|
try {
|
||||||
|
body = await req.json();
|
||||||
|
} catch {
|
||||||
|
return fail("VALIDATION", "Invalid JSON", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = CreateSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return fail("VALIDATION", parsed.error.issues[0]?.message ?? "Invalid", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const row = await createCcMessageTypeDict({
|
||||||
|
name: parsed.data.name,
|
||||||
|
enabled: parsed.data.enabled,
|
||||||
|
sortOrder: parsed.data.sort_order,
|
||||||
|
});
|
||||||
|
await writeAudit({
|
||||||
|
actor: guard.session.username!,
|
||||||
|
action: "CC_MESSAGE_TYPE_CREATE",
|
||||||
|
payload: { id: row.id, name: row.name },
|
||||||
|
});
|
||||||
|
return ok(row, 201);
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err instanceof Prisma.PrismaClientKnownRequestError &&
|
||||||
|
err.code === "P2002"
|
||||||
|
) {
|
||||||
|
return fail("CONFLICT", "留言类型名称已存在", 409);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { fail, ok, parseBigIntId, requireAdminOrCustomer } from "@/lib/api";
|
||||||
|
import { assertSafeCcApiBase } from "@/lib/cc-api-base";
|
||||||
|
import { writeAudit } from "@/services/audit";
|
||||||
|
import {
|
||||||
|
deleteCcCustomerProfile,
|
||||||
|
getCcCustomerProfile,
|
||||||
|
updateCcCustomerProfile,
|
||||||
|
} from "@/services/cc/customer-profiles";
|
||||||
|
import { sessionUserId } from "@/services/tenant/access";
|
||||||
|
|
||||||
|
const PutSchema = z.object({
|
||||||
|
name: z.string().min(1).max(128).optional(),
|
||||||
|
mock: z.boolean().optional(),
|
||||||
|
api_base: z.string().url().max(512).optional(),
|
||||||
|
saas_header: z.string().min(1).max(64).optional(),
|
||||||
|
login_mark: z.string().uuid().optional(),
|
||||||
|
username: z.string().max(128).optional(),
|
||||||
|
password: z.string().max(256).optional(),
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
sort_order: z.number().int().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type RouteContext = { params: Promise<{ id: string }> };
|
||||||
|
|
||||||
|
async function assertCanManageProfile(
|
||||||
|
session: { role: string; userId?: string | null },
|
||||||
|
profileId: bigint,
|
||||||
|
) {
|
||||||
|
const profile = await getCcCustomerProfile(profileId);
|
||||||
|
if (!profile) return { error: fail("NOT_FOUND", "客户档案不存在", 404) as Response };
|
||||||
|
if (session.role === "customer") {
|
||||||
|
const uid = sessionUserId(session as never);
|
||||||
|
if (!uid || profile.owner_user_id !== uid.toString()) {
|
||||||
|
return { error: fail("FORBIDDEN", "无权操作该客户档案", 403) as Response };
|
||||||
|
}
|
||||||
|
} else if (profile.owner_user_id != null && session.role !== "admin") {
|
||||||
|
return { error: fail("FORBIDDEN", "无权操作该客户档案", 403) as Response };
|
||||||
|
}
|
||||||
|
return { profile };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(req: Request, ctx: RouteContext) {
|
||||||
|
const guard = await requireAdminOrCustomer();
|
||||||
|
if (guard.response) return guard.response;
|
||||||
|
|
||||||
|
const { id } = await ctx.params;
|
||||||
|
const profileId = parseBigIntId(id);
|
||||||
|
if (!profileId) return fail("VALIDATION", "Invalid id", 400);
|
||||||
|
|
||||||
|
const access = await assertCanManageProfile(guard.session, profileId);
|
||||||
|
if (access.error) return access.error;
|
||||||
|
|
||||||
|
let body: unknown;
|
||||||
|
try {
|
||||||
|
body = await req.json();
|
||||||
|
} catch {
|
||||||
|
return fail("VALIDATION", "Invalid JSON", 400);
|
||||||
|
}
|
||||||
|
const parsed = PutSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return fail("VALIDATION", parsed.error.issues[0]?.message ?? "Invalid", 400);
|
||||||
|
}
|
||||||
|
const d = parsed.data;
|
||||||
|
|
||||||
|
let apiBase: string | undefined;
|
||||||
|
if (d.api_base) {
|
||||||
|
try {
|
||||||
|
apiBase = assertSafeCcApiBase(d.api_base);
|
||||||
|
} catch (err) {
|
||||||
|
return fail(
|
||||||
|
"VALIDATION",
|
||||||
|
err instanceof Error ? err.message : "非法 CC API 地址",
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await updateCcCustomerProfile(profileId, {
|
||||||
|
name: d.name,
|
||||||
|
mock: d.mock,
|
||||||
|
apiBase,
|
||||||
|
saasHeader: d.saas_header,
|
||||||
|
loginMark: d.login_mark,
|
||||||
|
username: d.username,
|
||||||
|
password: d.password,
|
||||||
|
enabled: d.enabled,
|
||||||
|
sortOrder: d.sort_order,
|
||||||
|
});
|
||||||
|
if (!item) return fail("NOT_FOUND", "客户档案不存在", 404);
|
||||||
|
|
||||||
|
await writeAudit({
|
||||||
|
actor: guard.session.username!,
|
||||||
|
action: "CC_PROFILE_UPDATE",
|
||||||
|
payload: { id: item.id, name: item.name },
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(_req: Request, ctx: RouteContext) {
|
||||||
|
const guard = await requireAdminOrCustomer();
|
||||||
|
if (guard.response) return guard.response;
|
||||||
|
|
||||||
|
const { id } = await ctx.params;
|
||||||
|
const profileId = parseBigIntId(id);
|
||||||
|
if (!profileId) return fail("VALIDATION", "Invalid id", 400);
|
||||||
|
|
||||||
|
const access = await assertCanManageProfile(guard.session, profileId);
|
||||||
|
if (access.error) return access.error;
|
||||||
|
|
||||||
|
const okDel = await deleteCcCustomerProfile(profileId);
|
||||||
|
if (!okDel) return fail("NOT_FOUND", "客户档案不存在", 404);
|
||||||
|
|
||||||
|
await writeAudit({
|
||||||
|
actor: guard.session.username!,
|
||||||
|
action: "CC_PROFILE_DELETE",
|
||||||
|
payload: { id: profileId.toString() },
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok({ deleted: true });
|
||||||
|
}
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Select, Typography } from "antd";
|
||||||
|
import { listCcCustomerProfilesApi } from "@/lib/client-api";
|
||||||
|
import type { CcCustomerProfile } from "@/types";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "yx.ccCustomerProfileId";
|
||||||
|
|
||||||
|
export function readStoredCcProfileId(): string | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(STORAGE_KEY);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeStoredCcProfileId(id: string | null) {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
if (id) localStorage.setItem(STORAGE_KEY, id);
|
||||||
|
else localStorage.removeItem(STORAGE_KEY);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCcCustomerProfileSelect(opts?: { mailId?: string }) {
|
||||||
|
const [items, setItems] = useState<CcCustomerProfile[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [profileId, setProfileIdState] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const reload = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await listCcCustomerProfilesApi({
|
||||||
|
selectable: true,
|
||||||
|
mailId: opts?.mailId,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
if (!res.ok) {
|
||||||
|
setItems([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setItems(res.data.items);
|
||||||
|
const stored = readStoredCcProfileId();
|
||||||
|
const valid =
|
||||||
|
stored && res.data.items.some((i) => i.id === stored) ? stored : null;
|
||||||
|
const next = valid || res.data.items[0]?.id || null;
|
||||||
|
setProfileIdState(next);
|
||||||
|
if (next) writeStoredCcProfileId(next);
|
||||||
|
}, [opts?.mailId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void reload();
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
|
const setProfileId = useCallback((id: string | null) => {
|
||||||
|
setProfileIdState(id);
|
||||||
|
writeStoredCcProfileId(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selected = useMemo(
|
||||||
|
() => items.find((i) => i.id === profileId) || null,
|
||||||
|
[items, profileId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
loading,
|
||||||
|
profileId,
|
||||||
|
setProfileId,
|
||||||
|
selected,
|
||||||
|
reload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CcCustomerProfileSelect({
|
||||||
|
mailId,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
items,
|
||||||
|
loading,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
mailId?: string;
|
||||||
|
value?: string | null;
|
||||||
|
onChange?: (id: string | null) => void;
|
||||||
|
items?: CcCustomerProfile[];
|
||||||
|
loading?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}) {
|
||||||
|
const internal = useCcCustomerProfileSelect(
|
||||||
|
items ? undefined : { mailId },
|
||||||
|
);
|
||||||
|
const list = items ?? internal.items;
|
||||||
|
const busy = loading ?? internal.loading;
|
||||||
|
const current = value !== undefined ? value : internal.profileId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
placeholder={list.length ? "选择 CC 客户" : "请先在设置中配置客户"}
|
||||||
|
loading={busy}
|
||||||
|
style={{ minWidth: 200, ...style }}
|
||||||
|
value={current || undefined}
|
||||||
|
options={list.map((i) => ({
|
||||||
|
value: i.id,
|
||||||
|
label: `${i.name}${i.username ? ` · ${i.username}` : ""}`,
|
||||||
|
}))}
|
||||||
|
onChange={(v) => {
|
||||||
|
const id = v || null;
|
||||||
|
if (onChange) onChange(id);
|
||||||
|
else internal.setProfileId(id);
|
||||||
|
if (id) writeStoredCcProfileId(id);
|
||||||
|
}}
|
||||||
|
notFoundContent={
|
||||||
|
<Typography.Text type="secondary">暂无可用客户档案</Typography.Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,17 @@
|
|||||||
|
/** 对齐 ClientContainerTemplate.xlsx / V1.72 SaveContainer 柜型枚举 */
|
||||||
|
export const CC_CABINET_TYPES = [
|
||||||
|
"20GP",
|
||||||
|
"40GP",
|
||||||
|
"40HQ",
|
||||||
|
"40HR",
|
||||||
|
"40HC",
|
||||||
|
"45HQ",
|
||||||
|
"53HQ",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type CcCabinetType = (typeof CC_CABINET_TYPES)[number];
|
||||||
|
|
||||||
|
export const CC_CABINET_TYPE_OPTIONS = CC_CABINET_TYPES.map((v) => ({
|
||||||
|
value: v,
|
||||||
|
label: v,
|
||||||
|
}));
|
||||||
@ -1,26 +1,51 @@
|
|||||||
import { AsyncLocalStorage } from "async_hooks";
|
import { AsyncLocalStorage } from "async_hooks";
|
||||||
import { prisma } from "@/services/db";
|
import { prisma } from "@/services/db";
|
||||||
|
|
||||||
const als = new AsyncLocalStorage<{ ownerUserId: bigint | null }>();
|
type CcOwnerStore = {
|
||||||
|
ownerUserId: bigint | null;
|
||||||
|
/** 选中的 CC 客户档案;优先于 ownerUserId 对应的 CcSettings */
|
||||||
|
profileId?: bigint | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const als = new AsyncLocalStorage<CcOwnerStore>();
|
||||||
|
|
||||||
export function getCcOwnerUserId(): bigint | null | undefined {
|
export function getCcOwnerUserId(): bigint | null | undefined {
|
||||||
return als.getStore()?.ownerUserId;
|
return als.getStore()?.ownerUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCcProfileId(): bigint | null | undefined {
|
||||||
|
const store = als.getStore();
|
||||||
|
if (!store) return undefined;
|
||||||
|
return store.profileId ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function runWithCcOwner<T>(
|
export function runWithCcOwner<T>(
|
||||||
ownerUserId: bigint | null,
|
ownerUserId: bigint | null,
|
||||||
fn: () => T,
|
fn: () => T,
|
||||||
): T {
|
): T {
|
||||||
return als.run({ ownerUserId }, fn);
|
return als.run({ ownerUserId, profileId: null }, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runWithCcProfile<T>(
|
||||||
|
profileId: bigint,
|
||||||
|
ownerUserId: bigint | null,
|
||||||
|
fn: () => T,
|
||||||
|
): T {
|
||||||
|
return als.run({ ownerUserId, profileId }, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runWithMailCcOwner<T>(
|
export async function runWithMailCcOwner<T>(
|
||||||
mailId: bigint,
|
mailId: bigint,
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
|
opts?: { profileId?: bigint | null },
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const mail = await prisma.mailMessage.findUnique({
|
const mail = await prisma.mailMessage.findUnique({
|
||||||
where: { id: mailId },
|
where: { id: mailId },
|
||||||
select: { mailboxAccount: { select: { ownerUserId: true } } },
|
select: { mailboxAccount: { select: { ownerUserId: true } } },
|
||||||
});
|
});
|
||||||
return runWithCcOwner(mail?.mailboxAccount?.ownerUserId ?? null, fn);
|
const ownerUserId = mail?.mailboxAccount?.ownerUserId ?? null;
|
||||||
|
if (opts?.profileId != null) {
|
||||||
|
return runWithCcProfile(opts.profileId, ownerUserId, fn);
|
||||||
|
}
|
||||||
|
return runWithCcOwner(ownerUserId, fn);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,101 @@
|
|||||||
|
import { prisma } from "@/services/db";
|
||||||
|
import { DEFAULT_CC_MESSAGE_TYPE_NAMES } from "@/constants/cc-message-types";
|
||||||
|
|
||||||
|
export type CcMessageTypeDictPublic = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
sort_order: number;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function toPublic(row: {
|
||||||
|
id: bigint;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
updatedAt: Date;
|
||||||
|
}): CcMessageTypeDictPublic {
|
||||||
|
return {
|
||||||
|
id: row.id.toString(),
|
||||||
|
name: row.name,
|
||||||
|
enabled: row.enabled,
|
||||||
|
sort_order: row.sortOrder,
|
||||||
|
updated_at: row.updatedAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 空表时写入默认种子(幂等) */
|
||||||
|
export async function ensureCcMessageTypeDictSeeded(): Promise<void> {
|
||||||
|
const count = await prisma.ccMessageTypeDict.count();
|
||||||
|
if (count > 0) return;
|
||||||
|
await prisma.ccMessageTypeDict.createMany({
|
||||||
|
data: DEFAULT_CC_MESSAGE_TYPE_NAMES.map((name, i) => ({
|
||||||
|
name,
|
||||||
|
enabled: true,
|
||||||
|
sortOrder: i,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listCcMessageTypeDict(opts?: {
|
||||||
|
enabledOnly?: boolean;
|
||||||
|
}): Promise<CcMessageTypeDictPublic[]> {
|
||||||
|
await ensureCcMessageTypeDictSeeded();
|
||||||
|
const rows = await prisma.ccMessageTypeDict.findMany({
|
||||||
|
where: opts?.enabledOnly ? { enabled: true } : undefined,
|
||||||
|
orderBy: [{ sortOrder: "asc" }, { id: "asc" }],
|
||||||
|
});
|
||||||
|
return rows.map(toPublic);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listEnabledCcMessageTypeOptions(): Promise<
|
||||||
|
Array<{ value: string; label: string }>
|
||||||
|
> {
|
||||||
|
const items = await listCcMessageTypeDict({ enabledOnly: true });
|
||||||
|
return items.map((i) => ({ value: i.name, label: i.name }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCcMessageTypeDict(input: {
|
||||||
|
name: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
sortOrder?: number;
|
||||||
|
}): Promise<CcMessageTypeDictPublic> {
|
||||||
|
await ensureCcMessageTypeDictSeeded();
|
||||||
|
const name = input.name.trim();
|
||||||
|
const maxSort = await prisma.ccMessageTypeDict.aggregate({
|
||||||
|
_max: { sortOrder: true },
|
||||||
|
});
|
||||||
|
const row = await prisma.ccMessageTypeDict.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
enabled: input.enabled ?? true,
|
||||||
|
sortOrder: input.sortOrder ?? (maxSort._max.sortOrder ?? 0) + 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return toPublic(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCcMessageTypeDict(
|
||||||
|
id: bigint,
|
||||||
|
input: { name?: string; enabled?: boolean; sortOrder?: number },
|
||||||
|
): Promise<CcMessageTypeDictPublic | null> {
|
||||||
|
const existing = await prisma.ccMessageTypeDict.findUnique({ where: { id } });
|
||||||
|
if (!existing) return null;
|
||||||
|
const row = await prisma.ccMessageTypeDict.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...(input.name !== undefined ? { name: input.name.trim() } : {}),
|
||||||
|
...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
|
||||||
|
...(input.sortOrder !== undefined ? { sortOrder: input.sortOrder } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return toPublic(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCcMessageTypeDict(id: bigint): Promise<boolean> {
|
||||||
|
const existing = await prisma.ccMessageTypeDict.findUnique({ where: { id } });
|
||||||
|
if (!existing) return false;
|
||||||
|
await prisma.ccMessageTypeDict.delete({ where: { id } });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue