feat(assoc): v4.0.1 合伙人关联码、分佣账单与 H5 用户管理
订单佣金只认关联用户;合伙人备注写入独立表;H5 增加用户管理与首页统计。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
IsArray,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
MaxLength,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
STORE_RATING_MAX_COMMENT,
|
||||
STORE_RATING_MAX_IMAGES,
|
||||
type SubmitStoreRatingRequest,
|
||||
} from '@dukang/shared-types';
|
||||
|
||||
export class SubmitStoreRatingDto implements SubmitStoreRatingRequest {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
redeemRecordId: string;
|
||||
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
serviceScore: number;
|
||||
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
envScore: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(STORE_RATING_MAX_COMMENT)
|
||||
comment?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMaxSize(8)
|
||||
@IsString({ each: true })
|
||||
tags?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMaxSize(STORE_RATING_MAX_IMAGES)
|
||||
@IsString({ each: true })
|
||||
imageUrls?: string[];
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
RedeemPhoneSendLookupSmsDto,
|
||||
} from './dto/phone-redeem.dto';
|
||||
import { RedeemFailureReportDto, RedeemPendingSubmitDto } from './dto/weaknet-redeem.dto';
|
||||
import { SubmitStoreRatingDto } from './dto/submit-rating.dto';
|
||||
|
||||
@Controller('redeem')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -33,8 +34,8 @@ export class UserRedeemController {
|
||||
}
|
||||
|
||||
@Post('ratings')
|
||||
rating(@CurrentUser() user: AuthUser, @Body() body: Record<string, unknown>) {
|
||||
return this.redeemService.submitRating(user.actorId, body as never);
|
||||
rating(@CurrentUser() user: AuthUser, @Body() body: SubmitStoreRatingDto) {
|
||||
return this.redeemService.submitRating(user.actorId, body);
|
||||
}
|
||||
|
||||
@Get('records')
|
||||
@@ -49,6 +50,11 @@ export class UserRedeemController {
|
||||
Number(pageSize),
|
||||
);
|
||||
}
|
||||
|
||||
@Get('records/:id')
|
||||
record(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.redeemService.getUserRecord(user.actorId, id);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('shop/redeem')
|
||||
|
||||
@@ -51,6 +51,42 @@ function maskRedeemUserLabel(phone?: string | null): string {
|
||||
return '用户***';
|
||||
}
|
||||
|
||||
function asStringList(raw: unknown): string[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map((item) => String(item).trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeRatingTags(raw: unknown): string[] {
|
||||
return [...new Set(asStringList(raw))].slice(0, 8);
|
||||
}
|
||||
|
||||
function normalizeRatingImageUrls(raw: unknown): string[] {
|
||||
return asStringList(raw)
|
||||
.filter((url) => /^https?:\/\//i.test(url))
|
||||
.slice(0, 6);
|
||||
}
|
||||
|
||||
function mapStoreRating(
|
||||
rating: {
|
||||
serviceScore: number;
|
||||
envScore: number;
|
||||
comment?: string | null;
|
||||
tags?: unknown;
|
||||
imageUrls?: unknown;
|
||||
createdAt?: Date;
|
||||
} | null,
|
||||
) {
|
||||
if (!rating) return null;
|
||||
return {
|
||||
serviceScore: rating.serviceScore,
|
||||
envScore: rating.envScore,
|
||||
comment: rating.comment ?? '',
|
||||
tags: asStringList(rating.tags),
|
||||
imageUrls: asStringList(rating.imageUrls),
|
||||
createdAt: rating.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
type PendingSnapshot = TokenPayload & {
|
||||
redeemType: 'DIRECT' | 'COUPON';
|
||||
};
|
||||
@@ -1264,7 +1300,7 @@ export class RedeemService {
|
||||
take,
|
||||
include: {
|
||||
store: { select: { id: true, name: true } },
|
||||
rating: { select: { serviceScore: true, envScore: true } },
|
||||
rating: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.redeemRecord.count({ where: { userId } }),
|
||||
@@ -1278,9 +1314,7 @@ export class RedeemService {
|
||||
storeId: r.storeId,
|
||||
storeName: r.store?.name ?? '门店',
|
||||
createdAt: r.createdAt,
|
||||
rating: r.rating
|
||||
? { serviceScore: r.rating.serviceScore, envScore: r.rating.envScore }
|
||||
: null,
|
||||
rating: mapStoreRating(r.rating),
|
||||
})),
|
||||
),
|
||||
total,
|
||||
@@ -1289,6 +1323,26 @@ export class RedeemService {
|
||||
};
|
||||
}
|
||||
|
||||
async getUserRecord(userId: bigint, recordId: string) {
|
||||
const record = await this.prisma.redeemRecord.findFirst({
|
||||
where: { id: BigInt(recordId), userId },
|
||||
include: {
|
||||
store: { select: { id: true, name: true } },
|
||||
rating: true,
|
||||
},
|
||||
});
|
||||
if (!record) throw new NotFoundException('核销记录不存在');
|
||||
return serializeBigInt({
|
||||
id: record.id,
|
||||
redeemNo: record.redeemNo,
|
||||
amount: Number(record.amount),
|
||||
storeId: record.storeId,
|
||||
storeName: record.store?.name ?? '门店',
|
||||
createdAt: record.createdAt,
|
||||
rating: mapStoreRating(record.rating),
|
||||
});
|
||||
}
|
||||
|
||||
/** C 端门店详情走马灯:脱敏用户 + 时间 + 金额 */
|
||||
async listPublicStoreRecentRedeems(storeId: bigint, limit = 20) {
|
||||
const take = Math.min(Math.max(limit, 1), 50);
|
||||
@@ -1334,7 +1388,17 @@ export class RedeemService {
|
||||
});
|
||||
}
|
||||
|
||||
async submitRating(userId: bigint, body: { redeemRecordId: string; serviceScore: number; envScore: number }) {
|
||||
async submitRating(
|
||||
userId: bigint,
|
||||
body: {
|
||||
redeemRecordId: string;
|
||||
serviceScore: number;
|
||||
envScore: number;
|
||||
comment?: string;
|
||||
tags?: string[];
|
||||
imageUrls?: string[];
|
||||
},
|
||||
) {
|
||||
const record = await this.prisma.redeemRecord.findFirst({
|
||||
where: { id: BigInt(body.redeemRecordId), userId },
|
||||
});
|
||||
@@ -1342,15 +1406,21 @@ export class RedeemService {
|
||||
const existing = await this.prisma.storeRating.findUnique({
|
||||
where: { redeemRecordId: record.id },
|
||||
});
|
||||
if (existing) return serializeBigInt(existing);
|
||||
if (existing) throw new BadRequestException('该笔核销已评价');
|
||||
const tags = normalizeRatingTags(body.tags);
|
||||
const imageUrls = normalizeRatingImageUrls(body.imageUrls);
|
||||
const comment = String(body.comment || '').trim().slice(0, 200) || null;
|
||||
const rating = await this.prisma.storeRating.create({
|
||||
data: {
|
||||
redeemRecordId: record.id,
|
||||
storeId: record.storeId,
|
||||
serviceScore: body.serviceScore,
|
||||
envScore: body.envScore,
|
||||
comment,
|
||||
tags,
|
||||
imageUrls,
|
||||
},
|
||||
});
|
||||
return serializeBigInt(rating);
|
||||
return serializeBigInt(mapStoreRating(rating));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user