feat: 完善广场功能

This commit is contained in:
2026-04-26 19:17:15 +08:00
parent 3e8e2353f0
commit eff52f6063
16 changed files with 833 additions and 23 deletions
-11
View File
@@ -1,16 +1,5 @@
// server/api/images/history/[id].get.ts - 生图历史详情接口:返回当前用户单条记录的展示字段与归档状态。
import type { IImageHistoryDetail } from "#shared/types/openai";
import {
clearNewApiAuthCookies,
createApiLogger,
createErrorResponse,
createSuccessResponse,
createUpstreamErrorResponse,
getImageGenerationDetail,
getNewApiUserIdFromCookie,
isUnauthorizedError,
toSafeLogError
} from "~~/server/utils";
/**
* GET /api/images/history/:id
@@ -0,0 +1,92 @@
// server/api/images/history/[id]/public.delete.ts - 生图历史取消公开接口:将当前用户广场发布记录软隐藏。
import type { IImagePublicStateData } from "#shared/types/openai";
import {
clearNewApiAuthCookies,
createApiLogger,
createErrorResponse,
createSuccessResponse,
createUpstreamErrorResponse,
getNewApiUserIdFromCookie,
hideImageGenerationFromPlaza,
isUnauthorizedError,
toSafeLogError
} from "~~/server/utils";
/**
* DELETE /api/images/history/:id/public
*
* 流程:
* 1. 从 httpOnly cookie 读取当前用户 ID。
* 2. 只允许取消公开当前用户、未软删除的生图记录。
* 3. 不删除广场发布记录,只改为隐藏并记录 hiddenAt。
* 4. 不返回上游响应、图床响应或内部错误详情。
*/
export default defineEventHandler(async (event) => {
const logger = createApiLogger("images.history.public.hide");
logger.info("开始");
try {
const userId = getNewApiUserIdFromCookie(event);
const recordId = parseRecordId(getRouterParam(event, "id"));
logger.info("取消公开参数", {
userId,
recordId: recordId.toString()
});
const result = await hideImageGenerationFromPlaza(userId, recordId);
if (!result) {
logger.warn("公开记录不存在", {
userId,
recordId: recordId.toString()
});
return createErrorResponse(404, "公开记录不存在");
}
logger.done("成功", {
userId,
recordId: recordId.toString()
});
return createSuccessResponse<IImagePublicStateData>(result, "已取消公开");
} catch (error) {
if (isUnauthorizedError(error)) {
clearNewApiAuthCookies(event);
logger.warn("未登录或登录态失效", {
error: toSafeLogError(error)
});
return createErrorResponse(401, "未登录");
}
if (isBadRequestError(error)) {
return createErrorResponse(400, "生图记录 ID 不正确");
}
logger.error("失败", {
error: toSafeLogError(error)
});
return createUpstreamErrorResponse(error, "取消公开图片失败");
}
});
/** 校验并解析路由里的生图记录 ID */
const parseRecordId = (value: string | undefined): bigint => {
if (!value || !/^\d+$/.test(value)) {
throw createError({
statusCode: 400,
statusMessage: "生图记录 ID 不正确"
});
}
return BigInt(value);
};
const isBadRequestError = (error: unknown): boolean => {
return (
typeof error === "object" &&
error !== null &&
"statusCode" in error &&
error.statusCode === 400
);
};
@@ -0,0 +1,100 @@
// server/api/images/history/[id]/public.post.ts - 生图历史公开接口:将当前用户单条生图记录发布到广场。
import type { IImagePublicStateData } from "#shared/types/openai";
import {
clearNewApiAuthCookies,
createApiLogger,
createErrorResponse,
createSuccessResponse,
createUpstreamErrorResponse,
getNewApiUserIdFromCookie,
isUnauthorizedError,
publishImageGenerationToPlaza,
toSafeLogError
} from "~~/server/utils";
/**
* POST /api/images/history/:id/public
*
* 流程:
* 1. 从 httpOnly cookie 读取当前用户 ID。
* 2. 只允许公开当前用户、未软删除的生图记录。
* 3. 允许图片尚未归档时先创建公开记录;广场列表会等 hostedImageUrl 可用后再展示。
* 4. 不返回上游响应、图床响应或内部错误详情。
*/
export default defineEventHandler(async (event) => {
const logger = createApiLogger("images.history.public.publish");
logger.info("开始");
try {
const userId = getNewApiUserIdFromCookie(event);
const recordId = parseRecordId(getRouterParam(event, "id"));
logger.info("公开参数", {
userId,
recordId: recordId.toString()
});
const result = await publishImageGenerationToPlaza(userId, recordId);
if (!result) {
logger.warn("记录不存在", {
userId,
recordId: recordId.toString()
});
return createErrorResponse(404, "生图记录不存在");
}
if (result === "REMOVED") {
logger.warn("记录已下架,不能重新公开", {
userId,
recordId: recordId.toString()
});
return createErrorResponse(403, "图片已下架,无法公开");
}
logger.done("成功", {
userId,
recordId: recordId.toString()
});
return createSuccessResponse<IImagePublicStateData>(result, "图片已公开");
} catch (error) {
if (isUnauthorizedError(error)) {
clearNewApiAuthCookies(event);
logger.warn("未登录或登录态失效", {
error: toSafeLogError(error)
});
return createErrorResponse(401, "未登录");
}
if (isBadRequestError(error)) {
return createErrorResponse(400, "生图记录 ID 不正确");
}
logger.error("失败", {
error: toSafeLogError(error)
});
return createUpstreamErrorResponse(error, "公开图片失败");
}
});
/** 校验并解析路由里的生图记录 ID */
const parseRecordId = (value: string | undefined): bigint => {
if (!value || !/^\d+$/.test(value)) {
throw createError({
statusCode: 400,
statusMessage: "生图记录 ID 不正确"
});
}
return BigInt(value);
};
const isBadRequestError = (error: unknown): boolean => {
return (
typeof error === "object" &&
error !== null &&
"statusCode" in error &&
error.statusCode === 400
);
};
+75
View File
@@ -0,0 +1,75 @@
// server/api/images/plaza.get.ts - 广场列表接口:分页返回所有用户已公开且已归档成功的生图作品。
import type { IPlazaPostListData } from "#shared/types/openai";
import {
createApiLogger,
createSuccessResponse,
createUpstreamErrorResponse,
listPlazaPosts,
toSafeLogError
} from "~~/server/utils";
const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 20;
const MAX_PAGE_SIZE = 50;
/**
* GET /api/images/plaza
*
* 流程:
* 1. 读取 page/size 查询参数,并限制最大 pageSize。
* 2. 只返回公开、生成成功、未删除、且已有图床归档地址的记录。
* 3. 返回广场展示字段,不返回上游响应、图床响应或内部错误详情。
*/
export default defineEventHandler(async (event) => {
const logger = createApiLogger("images.plaza.list");
logger.info("开始");
try {
const query = getQuery(event);
const page = normalizePositiveInt(query.page, DEFAULT_PAGE);
const pageSize = Math.min(
normalizePositiveInt(query.size, DEFAULT_PAGE_SIZE),
MAX_PAGE_SIZE
);
logger.info("查询参数", {
page,
pageSize
});
const result = await listPlazaPosts({
page,
pageSize
});
logger.done("成功", {
page,
pageSize,
total: result.total,
itemCount: result.items.length
});
return createSuccessResponse<IPlazaPostListData>(
result,
"获取广场图片成功"
);
} catch (error) {
logger.error("失败", {
error: toSafeLogError(error)
});
return createUpstreamErrorResponse(error, "获取广场图片失败");
}
});
/** 将 query 参数归一化为正整数,非法值回退到默认值 */
const normalizePositiveInt = (
value: unknown,
fallbackValue: number
): number => {
const rawValue = Array.isArray(value) ? value[0] : value;
const numberValue = Number.parseInt(String(rawValue ?? ""), 10);
return Number.isInteger(numberValue) && numberValue > 0
? numberValue
: fallbackValue;
};