feat(reviews): order-line reviews with merchant reply and platform moderation (add-product-reviews)

This commit is contained in:
Chengdong Zhang
2026-09-24 16:38:05 +08:00
parent 473d19d089
commit c532f87b03
34 changed files with 2055 additions and 36 deletions
+23
View File
@@ -45,6 +45,10 @@ import type {
ProductStatus,
PublicFlashSaleSession,
RedeemPointsBody,
Review,
ReviewInput,
ReviewSummary,
ReviewableItem,
FreightTemplate,
FreightTemplateInput,
Shipment,
@@ -252,6 +256,10 @@ export interface ApiClient {
reopenAftersale(id: string): Promise<Aftersale>;
submitAftersaleReturnTracking(id: string, body: AftersaleReturnTrackingBody): Promise<Aftersale>;
addAftersaleMessage(id: string, body: AftersaleMessageBody): Promise<AftersaleMessage>;
listProductReviews(productId: string, page?: number): Promise<Paged<Review>>;
getProductReviewSummary(productId: string): Promise<ReviewSummary>;
listReviewableItems(): Promise<ReviewableItem[]>;
createReview(body: ReviewInput): Promise<Review>;
/** Public points catalog: published products only. */
listPointsProducts(): Promise<IntegralProduct[]>;
/** Server-computed per-shop shipping fees for the current cart + address. */
@@ -317,6 +325,8 @@ export interface ApiClient {
createFreightTemplate(body: FreightTemplateInput): Promise<FreightTemplate>;
updateFreightTemplate(id: string, body: FreightTemplateInput): Promise<FreightTemplate>;
deleteFreightTemplate(id: string): Promise<void>;
listReviews(page?: number): Promise<Paged<Review>>;
replyReview(id: string, content: LocalizedText): Promise<Review>;
};
admin: {
listUsers(page?: number): Promise<Paged<User>>;
@@ -338,6 +348,9 @@ export interface ApiClient {
listAftersales(status?: AftersaleStatus): Promise<Aftersale[]>;
getAftersale(id: string): Promise<AftersaleDetail>;
arbitrateAftersale(id: string, outcome: AftersaleArbitration): Promise<Aftersale>;
listReviews(page?: number): Promise<Paged<Review>>;
hideReview(id: string): Promise<Review>;
deleteReview(id: string): Promise<void>;
};
}
@@ -404,6 +417,11 @@ export function createApi(opts: ApiClientOptions): ApiClient {
submitAftersaleReturnTracking: (id, body) =>
r("POST", `/aftersales/${id}/return-tracking`, body),
addAftersaleMessage: (id, body) => r("POST", `/aftersales/${id}/messages`, body),
listProductReviews: (productId, page = 1) =>
r("GET", `/products/${productId}/reviews`, undefined, { page }),
getProductReviewSummary: (productId) => r("GET", `/products/${productId}/review-summary`),
listReviewableItems: () => r("GET", "/me/reviewable"),
createReview: (body) => r("POST", "/reviews", body),
listPointsProducts: () => r("GET", "/points/products"),
listMyRedemptions: () => r("GET", "/points/redemptions"),
redeemPoints: (body) => r("POST", "/points/redemptions", body),
@@ -460,6 +478,8 @@ export function createApi(opts: ApiClientOptions): ApiClient {
createFreightTemplate: (body) => r("POST", "/shop/freight-templates", body),
updateFreightTemplate: (id, body) => r("PUT", `/shop/freight-templates/${id}`, body),
deleteFreightTemplate: (id) => r("DELETE", `/shop/freight-templates/${id}`),
listReviews: (page = 1) => r("GET", "/shop/reviews", undefined, { page }),
replyReview: (id, content) => r("POST", `/shop/reviews/${id}/reply`, { content }),
},
admin: {
listUsers: (page = 1) => r("GET", "/admin/users", undefined, { page }),
@@ -485,6 +505,9 @@ export function createApi(opts: ApiClientOptions): ApiClient {
getAftersale: (id) => r("GET", `/admin/aftersales/${id}`),
arbitrateAftersale: (id, outcome) =>
r("POST", `/admin/aftersales/${id}/arbitrate`, { outcome }),
listReviews: (page = 1) => r("GET", "/admin/reviews", undefined, { page }),
hideReview: (id) => r("POST", `/admin/reviews/${id}/hide`),
deleteReview: (id) => r("DELETE", `/admin/reviews/${id}`),
listPointsProducts: () => r("GET", "/admin/points/products"),
createPointsProduct: (body) => r("POST", "/admin/points/products", body),
updatePointsProduct: (id, body) => r("PUT", `/admin/points/products/${id}`, body),
+47
View File
@@ -777,3 +777,50 @@ export interface ShippingQuote {
shops: ShippingQuoteShop[];
total_minor: number;
}
// ---- reviews ----
export type ReviewStatus = "visible" | "hidden";
export interface Review {
id: string;
order_item_id: string;
order_id: string;
product_id: string;
shop_id: string;
user_id: string;
rating: number;
content: LocalizedText;
images: string[];
reply: LocalizedText | null;
reply_at: string | null;
status: ReviewStatus;
created_at: string;
reviewer_name: string;
}
/** SQL-aggregated over visible reviews; distribution keys are "1".."5". */
export interface ReviewSummary {
count: number;
avg_rating: number;
distribution: Record<string, number>;
}
/** A completed order line of mine that has no review yet. */
export interface ReviewableItem {
order_item_id: string;
order_id: string;
order_no: string;
product_id: string;
product_name: LocalizedText;
sku_code: string;
image: string | null;
created_at: string;
}
export interface ReviewInput {
order_item_id: string;
rating: number;
content: LocalizedText;
images?: string[];
}