2026-05-05 03:05:26 +08:00

273 lines
11 KiB
JavaScript

import { z } from "zod";
const resourceTypes = ["avatar_frame", "coin", "vehicle", "chat_bubble", "badge", "floating_screen", "gift"];
const walletAssetTypes = ["COIN", "DIAMOND"];
const optionalWalletAssetTypeSchema = z
.preprocess((value) => {
if (typeof value !== "string") {
return value;
}
const normalized = value.trim().toUpperCase();
return normalized || undefined;
}, z.string().optional())
.refine((value) => !value || walletAssetTypes.includes(value), "请选择金币或钻石");
export const resourceCreateFormSchema = z
.object({
assetUrl: z.string().trim().min(1, "请上传资源素材").max(512, "资源素材不能超过 512 个字符"),
enabled: z.boolean(),
name: z.string().trim().min(1, "请输入资源名称").max(128, "资源名称不能超过 128 个字符"),
previewUrl: z.string().trim().min(1, "请上传资源封面").max(512, "资源封面不能超过 512 个字符"),
resourceCode: z.string().trim().min(1, "请输入资源编码").max(96, "资源编码不能超过 96 个字符"),
resourceType: z.enum(resourceTypes, "请选择资源类型"),
walletAssetAmount: z.union([z.string(), z.number()]).optional(),
})
.superRefine((value, context) => {
if (value.resourceType !== "coin") {
return;
}
const amount = Number(value.walletAssetAmount);
if (!Number.isInteger(amount) || amount <= 0) {
context.addIssue({
code: "custom",
message: "请输入金币数量",
path: ["walletAssetAmount"],
});
}
});
export const resourceFormSchema = resourceCreateFormSchema;
const groupItemSchema = z.object({
durationDays: z.union([z.string(), z.number()]).optional(),
itemType: z.enum(["resource", "wallet_asset"]),
resourceId: z.union([z.string(), z.number()]).optional(),
sortOrder: z.union([z.string(), z.number()]).optional(),
walletAssetAmount: z.union([z.string(), z.number()]).optional(),
walletAssetType: optionalWalletAssetTypeSchema,
});
export const resourceGroupCreateFormSchema = z
.object({
description: z.string().trim().max(512, "资源组说明不能超过 512 个字符").optional(),
enabled: z.boolean(),
groupCode: z.string().trim().min(1, "请输入资源组编码").max(96, "资源组编码不能超过 96 个字符"),
items: z.array(groupItemSchema).min(1, "请至少配置一个资源组条目"),
name: z.string().trim().min(1, "请输入资源组名称").max(128, "资源组名称不能超过 128 个字符"),
sortOrder: z.union([z.string(), z.number()]).optional(),
})
.superRefine((value, context) => {
const resourceIds = new Set();
const walletAssets = new Set();
value.items.forEach((item, index) => {
if (item.itemType === "wallet_asset") {
const amount = Number(item.walletAssetAmount);
if (!item.walletAssetType || !Number.isInteger(amount) || amount <= 0) {
context.addIssue({
code: "custom",
message: "请输入金币或钻石数量",
path: ["items", index, "walletAssetAmount"],
});
}
if (item.walletAssetType) {
if (walletAssets.has(item.walletAssetType)) {
context.addIssue({
code: "custom",
message: "同一种钱包资产不能重复配置",
path: ["items", index, "walletAssetType"],
});
}
walletAssets.add(item.walletAssetType);
}
return;
}
const resourceId = Number(item.resourceId);
const durationDays = Number(item.durationDays);
if (!Number.isInteger(resourceId) || resourceId <= 0) {
context.addIssue({
code: "custom",
message: "请选择资源",
path: ["items", index, "resourceId"],
});
}
if (!Number.isInteger(durationDays) || durationDays <= 0) {
context.addIssue({
code: "custom",
message: "请输入有效天数",
path: ["items", index, "durationDays"],
});
}
if (resourceIds.has(resourceId)) {
context.addIssue({
code: "custom",
message: "同一个资源不能重复配置",
path: ["items", index, "resourceId"],
});
}
resourceIds.add(resourceId);
});
});
export const giftFormSchema = z
.object({
chargeAssetType: z.enum(["COIN", "DIAMOND"]),
coinPrice: z.union([z.string(), z.number()]),
effectTypes: z.array(z.enum(["animation", "music", "global_broadcast"])).optional(),
effectiveFrom: z.string().optional(),
effectiveTo: z.string().optional(),
enabled: z.boolean(),
giftId: z.string().trim().min(1, "请输入礼物 ID").max(96, "礼物 ID 不能超过 96 个字符"),
giftPointAmount: z.union([z.string(), z.number()]).optional(),
giftTypeCode: z.enum(["normal", "cp", "lucky", "super_lucky", "exclusive", "noble", "flag", "activity", "magic", "custom"]),
heatValue: z.union([z.string(), z.number()]).optional(),
name: z.string().trim().min(1, "请输入礼物名称").max(128, "礼物名称不能超过 128 个字符"),
presentationJson: z.string().trim().optional(),
priceVersion: z.string().trim().min(1, "请输入价格版本").max(64, "价格版本不能超过 64 个字符"),
regionIds: z.array(z.union([z.string(), z.number()])).min(1, "请选择区域"),
resourceId: z.union([z.string(), z.number()]),
sortOrder: z.union([z.string(), z.number()]).optional(),
})
.superRefine((value, context) => {
const resourceId = Number(value.resourceId);
const coinPrice = Number(value.coinPrice);
const giftPointAmount = Number(value.giftPointAmount || 0);
const heatValue = Number(value.heatValue || 0);
const effectiveFromMs = datetimeLocalToMs(value.effectiveFrom);
const effectiveToMs = datetimeLocalToMs(value.effectiveTo);
if (!Number.isInteger(resourceId) || resourceId <= 0) {
context.addIssue({
code: "custom",
message: "请选择礼物资源",
path: ["resourceId"],
});
}
if (!Number.isInteger(coinPrice) || coinPrice <= 0) {
context.addIssue({
code: "custom",
message: "请输入价格",
path: ["coinPrice"],
});
}
if (!Number.isInteger(giftPointAmount) || giftPointAmount < 0) {
context.addIssue({
code: "custom",
message: "请输入有效积分",
path: ["giftPointAmount"],
});
}
if (!Number.isInteger(heatValue) || heatValue < 0) {
context.addIssue({
code: "custom",
message: "请输入有效热度",
path: ["heatValue"],
});
}
if (effectiveFromMs < 0) {
context.addIssue({
code: "custom",
message: "请选择有效开始时间",
path: ["effectiveFrom"],
});
}
if (effectiveToMs < 0) {
context.addIssue({
code: "custom",
message: "请选择有效结束时间",
path: ["effectiveTo"],
});
}
if (effectiveFromMs > 0 && effectiveToMs > 0 && effectiveToMs <= effectiveFromMs) {
context.addIssue({
code: "custom",
message: "有效结束时间必须晚于开始时间",
path: ["effectiveTo"],
});
}
value.regionIds.forEach((regionId, index) => {
const normalizedRegionId = Number(regionId);
if (!Number.isInteger(normalizedRegionId) || normalizedRegionId < 0) {
context.addIssue({
code: "custom",
message: "请选择区域",
path: ["regionIds", index],
});
}
});
if (value.presentationJson) {
try {
JSON.parse(value.presentationJson);
} catch {
context.addIssue({
code: "custom",
message: "展示配置必须是合法 JSON",
path: ["presentationJson"],
});
}
}
});
function datetimeLocalToMs(value) {
const trimmed = String(value || "").trim();
if (!trimmed) {
return 0;
}
const parsed = new Date(trimmed).getTime();
return Number.isFinite(parsed) ? parsed : -1;
}
export const resourceGrantFormSchema = z
.object({
durationDays: z.union([z.string(), z.number()]).optional(),
groupId: z.union([z.string(), z.number()]).optional(),
quantity: z.union([z.string(), z.number()]).optional(),
reason: z.string().trim().min(1, "请输入原因").max(240, "原因不能超过 240 个字符"),
resourceId: z.union([z.string(), z.number()]).optional(),
subjectType: z.enum(["resource", "resource_group"]),
targetUserId: z.union([z.string(), z.number()]),
})
.superRefine((value, context) => {
const targetUserId = Number(value.targetUserId);
const resourceId = Number(value.resourceId || 0);
const groupId = Number(value.groupId || 0);
const quantity = Number(value.quantity || 1);
const durationDays = Number(value.durationDays || 0);
if (!Number.isInteger(targetUserId) || targetUserId <= 0) {
context.addIssue({
code: "custom",
message: "请输入用户 ID",
path: ["targetUserId"],
});
}
if (value.subjectType === "resource" && (!Number.isInteger(resourceId) || resourceId <= 0)) {
context.addIssue({
code: "custom",
message: "请选择资源",
path: ["resourceId"],
});
}
if (value.subjectType === "resource_group" && (!Number.isInteger(groupId) || groupId <= 0)) {
context.addIssue({
code: "custom",
message: "请选择资源组",
path: ["groupId"],
});
}
if (!Number.isInteger(quantity) || quantity <= 0) {
context.addIssue({
code: "custom",
message: "请输入数量",
path: ["quantity"],
});
}
if (!Number.isFinite(durationDays) || durationDays < 0) {
context.addIssue({
code: "custom",
message: "请输入有效天数",
path: ["durationDays"],
});
}
});