feat: audit diff, mock SMS 999888, env photos, proxy pay lock, mini-user UX
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { SMS_CODE_TTL_SECONDS } from '@dukang/shared-types';
|
||||
import { MOCK_SMS_FIXED_CODE, SMS_CODE_TTL_SECONDS } from '@dukang/shared-types';
|
||||
import { RedisService } from '../../common/redis/redis.service';
|
||||
|
||||
export { MOCK_SMS_FIXED_CODE };
|
||||
|
||||
const RATE_TTL_SECONDS = 60;
|
||||
|
||||
function codeKey(phone: string, scene: string) {
|
||||
@@ -31,12 +33,15 @@ export class SmsCodeStore {
|
||||
await this.redis.client.set(rateKey(phone), '1', 'EX', RATE_TTL_SECONDS);
|
||||
}
|
||||
|
||||
async generateAndStore(phone: string, scene: string): Promise<string> {
|
||||
const code = randomSixDigitCode();
|
||||
async storeCode(phone: string, scene: string, code: string): Promise<string> {
|
||||
await this.redis.client.set(codeKey(phone, scene), code, 'EX', SMS_CODE_TTL_SECONDS);
|
||||
return code;
|
||||
}
|
||||
|
||||
async generateAndStore(phone: string, scene: string): Promise<string> {
|
||||
return this.storeCode(phone, scene, randomSixDigitCode());
|
||||
}
|
||||
|
||||
async verifyAndConsume(phone: string, scene: string, code: string) {
|
||||
const key = codeKey(phone, scene);
|
||||
const stored = await this.redis.client.get(key);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { MockSmsCodeService } from '../../common/mock-sms-code/mock-sms-code.service';
|
||||
import type { ISmsProvider, SmsActorRef, SmsSendResult } from './sms.interface';
|
||||
import { SmsCodeStore } from './sms-code.store';
|
||||
import { MOCK_SMS_FIXED_CODE, SmsCodeStore } from './sms-code.store';
|
||||
|
||||
function maskPhone(phone: string) {
|
||||
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
|
||||
@@ -19,7 +19,7 @@ export class SmsMockProvider implements ISmsProvider {
|
||||
) {}
|
||||
|
||||
async send(phone: string, scene: string, actorRef?: SmsActorRef): Promise<SmsSendResult> {
|
||||
const code = await this.smsCodeStore.generateAndStore(phone, scene);
|
||||
const code = await this.smsCodeStore.storeCode(phone, scene, MOCK_SMS_FIXED_CODE);
|
||||
await this.mockSmsCodeService.record(phone, scene, code);
|
||||
const masked = maskPhone(phone);
|
||||
this.logger.log(`Mock SMS → ${masked} scene=${scene} code=${code}`);
|
||||
|
||||
@@ -83,6 +83,11 @@ export class AdminStorePackageAuditController {
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':requestId')
|
||||
detail(@Param('requestId') requestId: string) {
|
||||
return this.packages.adminGetAuditDetail(BigInt(requestId));
|
||||
}
|
||||
|
||||
@Put(':requestId/audit')
|
||||
audit(
|
||||
@CurrentUser() user: AuthUser,
|
||||
|
||||
@@ -221,6 +221,28 @@ export class StorePackageService {
|
||||
]);
|
||||
}
|
||||
|
||||
async adminGetAuditDetail(requestId: bigint) {
|
||||
const req = await this.prisma.storePackageChangeRequest.findUnique({
|
||||
where: { id: requestId },
|
||||
include: { store: { select: { id: true, name: true } } },
|
||||
});
|
||||
if (!req) throw new NotFoundException('审核记录不存在');
|
||||
const livePackages = await this.listLivePackages(req.storeId);
|
||||
return serializeBigInt({
|
||||
id: req.id.toString(),
|
||||
storeId: req.storeId.toString(),
|
||||
storeName: req.store.name,
|
||||
status: req.status,
|
||||
packages: req.packagesJson as unknown as StorePackageItemDto[],
|
||||
livePackages,
|
||||
submitterType: req.submitterType,
|
||||
submitterId: req.submitterId.toString(),
|
||||
rejectReason: req.rejectReason,
|
||||
reviewedAt: req.reviewedAt?.toISOString() ?? null,
|
||||
createdAt: req.createdAt.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
async adminListAudits(query: { status?: string; page?: number; pageSize?: number }) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
|
||||
@@ -670,7 +670,7 @@ export class StoreService {
|
||||
return this.partnerGetStore(partnerAccountId, storeId);
|
||||
}
|
||||
|
||||
/** 重新上传门头照 / 环境照:先软删旧 ENV,再写入去重后的新图(最多 3 张) */
|
||||
/** 重新上传门头照 / 环境照:先软删旧 ENV,再写入去重后的新图(至少 3 张,不设上限) */
|
||||
async partnerUpdateStoreMedia(
|
||||
partnerAccountId: bigint,
|
||||
storeId: bigint,
|
||||
@@ -1271,7 +1271,7 @@ export class StoreService {
|
||||
await this.assertStoreOwnedByAccount(partnerAccountId, storeId);
|
||||
}
|
||||
|
||||
private normalizeEnvPhotoUrls(raw: unknown, max = 3): string[] {
|
||||
private normalizeEnvPhotoUrls(raw: unknown): string[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const seen = new Set<string>();
|
||||
const urls: string[] = [];
|
||||
@@ -1280,7 +1280,6 @@ export class StoreService {
|
||||
if (!url || seen.has(url)) continue;
|
||||
seen.add(url);
|
||||
urls.push(url);
|
||||
if (urls.length >= max) break;
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
@@ -258,6 +258,11 @@ export class PartnerProxyOrderController {
|
||||
return this.tradeService.payPartnerProxyOrder(user.actorId, BigInt(id), dto.payMethod);
|
||||
}
|
||||
|
||||
@Post(':id/cancel-pay')
|
||||
cancelPay(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.tradeService.cancelPartnerProxyOrder(user.actorId, BigInt(id));
|
||||
}
|
||||
|
||||
@Post(':id/pay/mock-confirm')
|
||||
mockConfirmPay(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.tradeService.mockConfirmProxyPay(BigInt(id), {
|
||||
|
||||
@@ -1545,7 +1545,52 @@ export class TradeService {
|
||||
where: orderStatusLogWhere(orderId),
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return serializeBigInt(mapOrderCompat({ ...order, statusLogs: mapStatusLogCompat(statusLogs) }));
|
||||
return serializeBigInt({
|
||||
...mapOrderCompat({ ...order, statusLogs: mapStatusLogCompat(statusLogs) }),
|
||||
proxyPayMethod: this.parseProxyPayMethod(order.channelSource),
|
||||
});
|
||||
}
|
||||
|
||||
private parseProxyPayMethod(channelSource: string | null | undefined): 'NATIVE' | 'JSAPI' | null {
|
||||
if (!channelSource) return null;
|
||||
const m = channelSource.match(/^PROXY_ONLINE:(NATIVE|JSAPI)$/);
|
||||
return m ? (m[1] as 'NATIVE' | 'JSAPI') : null;
|
||||
}
|
||||
|
||||
private proxyChannelWithMethod(method: 'NATIVE' | 'JSAPI'): string {
|
||||
return `PROXY_ONLINE:${method}`;
|
||||
}
|
||||
|
||||
/** 合伙人取消代下单支付(关闭待支付订单,可重新下单并选择其他支付方式) */
|
||||
async cancelPartnerProxyOrder(partnerAccountId: bigint, orderId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const order = await this.prisma.order.findFirst({
|
||||
where: {
|
||||
id: orderId,
|
||||
orderType: 'PROXY',
|
||||
proxyPartnerAccountId: primary.id,
|
||||
},
|
||||
});
|
||||
if (!order) throw new NotFoundException('代下单不存在');
|
||||
if (order.status !== 'PENDING_PAY' || order.payStatus !== 'UNPAID') {
|
||||
throw new BadRequestException('仅待支付订单可取消');
|
||||
}
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.order.update({
|
||||
where: { id: orderId },
|
||||
data: { status: 'CANCELLED', cancelledAt: new Date() },
|
||||
});
|
||||
await tx.commonEvent.create({
|
||||
data: buildOrderStatusEvent({
|
||||
orderId,
|
||||
fromStatus: 'PENDING_PAY',
|
||||
toStatus: 'CANCELLED',
|
||||
operator: 'PARTNER_PROXY',
|
||||
remark: '合伙人取消代下单支付',
|
||||
}),
|
||||
});
|
||||
});
|
||||
return serializeBigInt({ id: orderId.toString(), status: 'CANCELLED' });
|
||||
}
|
||||
|
||||
/** HQ 代下单:商品/推广码选项(运营侧可看白名单测试酒) */
|
||||
@@ -1775,6 +1820,21 @@ export class TradeService {
|
||||
throw new BadRequestException('订单已超时未支付');
|
||||
}
|
||||
|
||||
const lockedMethod = this.parseProxyPayMethod(order.channelSource);
|
||||
if (lockedMethod && lockedMethod !== payMethod) {
|
||||
throw new BadRequestException(
|
||||
lockedMethod === 'JSAPI'
|
||||
? '该订单已发起微信代付,请先取消支付后重新下单'
|
||||
: '该订单已生成收款码,请先取消支付后重新下单',
|
||||
);
|
||||
}
|
||||
if (!lockedMethod) {
|
||||
await this.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: { channelSource: this.proxyChannelWithMethod(payMethod) },
|
||||
});
|
||||
}
|
||||
|
||||
this.payRedeemAnomaly.onPayAttempt(Number(order.payAmount), {
|
||||
orderNo: order.orderNo,
|
||||
userId: order.userId,
|
||||
|
||||
Reference in New Issue
Block a user