feat: multi-module iteration
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginPasswordDto, LoginSmsDto, LoginWechatDto, SendSmsDto } from './dto/auth.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { ClientApp } from '@dukang/shared-types';
|
||||
|
||||
@Controller('admin/auth')
|
||||
export class AdminAuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('sms/send')
|
||||
sendSms(@Body() dto: SendSmsDto) {
|
||||
return this.authService.sendSms(dto.phone, dto.scene, { clientApp: ClientApp.HQ_WEB });
|
||||
}
|
||||
|
||||
@Post('login/sms')
|
||||
login(@Body() dto: LoginSmsDto) {
|
||||
return this.authService.loginHq(dto.phone, dto.code, ClientApp.HQ_WEB);
|
||||
}
|
||||
|
||||
@Post('login/password')
|
||||
loginPassword(@Body() dto: LoginPasswordDto) {
|
||||
return this.authService.loginHqPassword(dto.loginName, dto.password, ClientApp.HQ_WEB);
|
||||
}
|
||||
|
||||
@Post('login/wechat')
|
||||
wechatLogin(@Body() dto: LoginWechatDto) {
|
||||
return this.authService.loginHqWechat(dto.code, ClientApp.HQ_WEB, dto.platform ?? 'h5');
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
me(@CurrentUser() user: AuthUser) {
|
||||
return this.authService.getMe(user.actorType, user.actorId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import { BadRequestException, Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { AuthService } from './auth.service';
|
||||
import {
|
||||
BindPhoneDto,
|
||||
BindWechatDto,
|
||||
BindWechatPhoneDto,
|
||||
MiniWechatProfileDto,
|
||||
BootstrapSessionDto,
|
||||
CheckPartnerPhoneDto,
|
||||
LoginSmsDto,
|
||||
LoginWechatDto,
|
||||
LoginWechatPhoneDto,
|
||||
RefreshTokenDto,
|
||||
SendSmsDto,
|
||||
} from './dto/auth.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { ClientApp } from '@dukang/shared-types';
|
||||
|
||||
function resolveUserClientApp(req: Request): ClientApp {
|
||||
const header = String(req.headers['x-client-app'] || '').trim();
|
||||
if (header === ClientApp.USER_MINI) return ClientApp.USER_MINI;
|
||||
return ClientApp.USER_H5;
|
||||
}
|
||||
|
||||
@Controller()
|
||||
export class UserAuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('auth/session/bootstrap')
|
||||
bootstrap(@Req() req: Request, @Body() dto: BootstrapSessionDto) {
|
||||
return this.authService.bootstrapSession(dto.deviceKey, resolveUserClientApp(req));
|
||||
}
|
||||
|
||||
@Post('auth/token/refresh')
|
||||
refresh(@Req() req: Request, @Body() dto: RefreshTokenDto) {
|
||||
return this.authService.refreshAccessToken(dto.refreshToken, resolveUserClientApp(req));
|
||||
}
|
||||
|
||||
@Post('auth/sms/send')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
sendSms(@Req() req: Request, @Body() dto: SendSmsDto) {
|
||||
const guest = (req as Request & { user?: AuthUser }).user;
|
||||
const guestId = guest?.actorType === 'USER' ? guest.actorId : undefined;
|
||||
return this.authService.sendSms(dto.phone, dto.scene, {
|
||||
guestUserId: guestId,
|
||||
clientApp: resolveUserClientApp(req),
|
||||
});
|
||||
}
|
||||
|
||||
@Post('auth/login/sms')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
login(@Req() req: Request, @Body() dto: LoginSmsDto) {
|
||||
const guest = (req as Request & { user?: AuthUser }).user;
|
||||
const guestId = guest?.actorType === 'USER' ? guest.actorId : undefined;
|
||||
return this.authService.loginUser(dto.phone, dto.code, resolveUserClientApp(req), guestId);
|
||||
}
|
||||
|
||||
@Post('auth/phone/bind')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
bindPhone(@Req() req: Request, @CurrentUser() user: AuthUser, @Body() dto: BindPhoneDto) {
|
||||
return this.authService.bindPhone(user.actorId, dto.phone, dto.code, resolveUserClientApp(req));
|
||||
}
|
||||
|
||||
@Post('auth/login/wechat')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
wechatLogin(@Req() req: Request, @Body() dto: LoginWechatDto) {
|
||||
const guest = (req as Request & { user?: AuthUser }).user;
|
||||
const guestId = guest?.actorType === 'USER' ? guest.actorId : undefined;
|
||||
const clientApp = resolveUserClientApp(req);
|
||||
const platform = dto.platform ?? (clientApp === ClientApp.USER_MINI ? 'mini' : 'h5');
|
||||
return this.authService.loginUserWechat(dto.code, clientApp, platform, guestId);
|
||||
}
|
||||
|
||||
/** 小程序手机号快捷登录(getPhoneNumber) */
|
||||
@Post('auth/login/wechat-phone')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
wechatPhoneLogin(@Req() req: Request, @Body() dto: LoginWechatPhoneDto) {
|
||||
const guest = (req as Request & { user?: AuthUser }).user;
|
||||
const guestId = guest?.actorType === 'USER' ? guest.actorId : undefined;
|
||||
const clientApp = resolveUserClientApp(req);
|
||||
const platform = dto.platform ?? (clientApp === ClientApp.USER_MINI ? 'mini' : 'h5');
|
||||
return this.authService.loginUserWechatPhone(
|
||||
dto.phoneCode,
|
||||
clientApp,
|
||||
platform,
|
||||
guestId,
|
||||
dto.loginCode,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('auth/wechat/bind-phone')
|
||||
bindPhoneLegacy(@Req() req: Request, @Body() dto: BindWechatPhoneDto) {
|
||||
return this.authService.bindWechatPhone(
|
||||
dto.wxSessionKey,
|
||||
dto.phone,
|
||||
dto.code,
|
||||
resolveUserClientApp(req),
|
||||
);
|
||||
}
|
||||
|
||||
@Post('auth/wechat/bind')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
bindWechat(@Req() req: Request, @CurrentUser() user: AuthUser, @Body() dto: BindWechatDto) {
|
||||
const clientApp = resolveUserClientApp(req);
|
||||
const platform = dto.platform ?? (clientApp === ClientApp.USER_MINI ? 'mini' : 'h5');
|
||||
return this.authService.bindUserWechat(
|
||||
user.actorId,
|
||||
{ code: dto.code, wxSessionKey: dto.wxSessionKey },
|
||||
clientApp,
|
||||
platform,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('auth/wechat/mini-profile')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
updateMiniWechatProfile(
|
||||
@Req() req: Request,
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Body() dto: MiniWechatProfileDto,
|
||||
) {
|
||||
if (user.actorType !== 'USER') {
|
||||
throw new BadRequestException('仅用户可更新资料');
|
||||
}
|
||||
const clientApp = resolveUserClientApp(req);
|
||||
if (clientApp !== ClientApp.USER_MINI) {
|
||||
throw new BadRequestException('仅小程序端可调用');
|
||||
}
|
||||
return this.authService.updateMiniWechatProfile(user.actorId, dto);
|
||||
}
|
||||
|
||||
@Get('auth/me')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
me(@CurrentUser() user: AuthUser) {
|
||||
return this.authService.getMe(user.actorType, user.actorId);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('shop/auth')
|
||||
export class ShopAuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('sms/send')
|
||||
sendSms(@Body() dto: SendSmsDto) {
|
||||
return this.authService.sendSms(dto.phone, dto.scene, { clientApp: ClientApp.SHOP_H5 });
|
||||
}
|
||||
|
||||
@Post('login/sms')
|
||||
login(@Body() dto: LoginSmsDto) {
|
||||
return this.authService.loginStore(dto.phone, dto.code, ClientApp.SHOP_H5);
|
||||
}
|
||||
|
||||
@Post('login/wechat')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
wechatLogin(@Req() req: Request, @Body() dto: LoginWechatDto) {
|
||||
const user = (req as Request & { user?: AuthUser }).user;
|
||||
if (user?.actorType === 'STORE') {
|
||||
return this.authService.bindStoreWechat(
|
||||
user.actorId,
|
||||
dto.code,
|
||||
ClientApp.SHOP_H5,
|
||||
dto.platform ?? 'h5',
|
||||
user.storeId,
|
||||
);
|
||||
}
|
||||
return this.authService.loginStoreWechat(dto.code, ClientApp.SHOP_H5, dto.platform ?? 'h5');
|
||||
}
|
||||
|
||||
@Post('token/refresh')
|
||||
refresh(@Body() dto: RefreshTokenDto) {
|
||||
return this.authService.refreshAccessToken(dto.refreshToken, ClientApp.SHOP_H5);
|
||||
}
|
||||
|
||||
@Get('stores')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
stores(@CurrentUser() user: AuthUser) {
|
||||
return this.authService.listShopStores(user.actorId);
|
||||
}
|
||||
|
||||
@Post('select-store')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
selectStore(@CurrentUser() user: AuthUser, @Body() body: { storeId: string }) {
|
||||
if (!body?.storeId) throw new BadRequestException('请选择门店');
|
||||
return this.authService.selectShopStore(user.actorId, BigInt(body.storeId), ClientApp.SHOP_H5);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
me(@CurrentUser() user: AuthUser) {
|
||||
return this.authService.getShopMe(user);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('partner/auth')
|
||||
export class PartnerAuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('phone/check')
|
||||
checkPhone(@Body() dto: CheckPartnerPhoneDto) {
|
||||
return this.authService.checkPartnerPhone(dto.phone);
|
||||
}
|
||||
|
||||
@Post('sms/send')
|
||||
sendSms(@Body() dto: SendSmsDto) {
|
||||
return this.authService.sendSms(dto.phone, dto.scene, { clientApp: ClientApp.PARTNER_H5 });
|
||||
}
|
||||
|
||||
@Post('login/sms')
|
||||
login(@Body() dto: LoginSmsDto) {
|
||||
return this.authService.loginPartner(dto.phone, dto.code, ClientApp.PARTNER_H5);
|
||||
}
|
||||
|
||||
@Post('login/wechat')
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
wechatLogin(@Req() req: Request, @Body() dto: LoginWechatDto) {
|
||||
const user = (req as Request & { user?: AuthUser }).user;
|
||||
if (user?.actorType === 'PARTNER') {
|
||||
return this.authService.bindPartnerWechat(
|
||||
user.actorId,
|
||||
dto.code,
|
||||
ClientApp.PARTNER_H5,
|
||||
dto.platform ?? 'h5',
|
||||
);
|
||||
}
|
||||
return this.authService.loginPartnerWechat(dto.code, ClientApp.PARTNER_H5, dto.platform ?? 'h5');
|
||||
}
|
||||
|
||||
@Post('token/refresh')
|
||||
refresh(@Body() dto: RefreshTokenDto) {
|
||||
return this.authService.refreshAccessToken(dto.refreshToken, ClientApp.PARTNER_H5);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('user')
|
||||
export class UserProfileController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Get('profile')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
profile(@CurrentUser() user: AuthUser) {
|
||||
return this.authService.getMe(user.actorType, user.actorId);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { SmsScene } from '@dukang/shared-types';
|
||||
|
||||
export class SendSmsDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsIn(Object.values(SmsScene))
|
||||
scene: string;
|
||||
}
|
||||
|
||||
export class LoginSmsDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
}
|
||||
|
||||
export class BootstrapSessionDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
deviceKey?: string;
|
||||
}
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export class BindPhoneDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
}
|
||||
|
||||
export class LoginWechatDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['h5', 'mini'])
|
||||
@IsOptional()
|
||||
platform?: 'h5' | 'mini';
|
||||
}
|
||||
|
||||
/** 小程序 getPhoneNumber 返回的 phoneCode,可选附带 wx.login code 绑定 openId */
|
||||
export class LoginWechatPhoneDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phoneCode: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
loginCode?: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['h5', 'mini'])
|
||||
@IsOptional()
|
||||
platform?: 'h5' | 'mini';
|
||||
}
|
||||
|
||||
export class BindWechatPhoneDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
wxSessionKey: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
}
|
||||
|
||||
export class LoginPasswordDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
loginName: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class BindWechatDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
code?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
wxSessionKey?: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['h5', 'mini'])
|
||||
@IsOptional()
|
||||
platform?: 'h5' | 'mini';
|
||||
}
|
||||
|
||||
export class MiniWechatProfileDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
nickname?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatarUrl?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatarResourceId?: string;
|
||||
}
|
||||
|
||||
export class CheckPartnerPhoneDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { IsArray, IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { AccountStatus, PartnerStaffRole } from '@dukang/shared-types';
|
||||
|
||||
export class SendPartnerStaffPhoneSmsDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export class CreatePartnerStaffDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
smsCode: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
/** 未传时服务端默认 INTERNAL */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(Object.values(PartnerStaffRole))
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class UpdatePartnerStaffDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(PartnerStaffRole))
|
||||
@IsOptional()
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(AccountStatus))
|
||||
@IsOptional()
|
||||
status?: string;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { IsArray, IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { AccountStatus, StoreStaffRole } from '@dukang/shared-types';
|
||||
|
||||
export class CreateStoreStaffDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
storeIds: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(Object.values(StoreStaffRole))
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class UpdateStoreStaffDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(StoreStaffRole))
|
||||
@IsOptional()
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(AccountStatus))
|
||||
@IsOptional()
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
storeIds?: string[];
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { IntegrationsModule } from '../../integrations/integrations.module';
|
||||
import { AnalyticsModule } from '../analytics/analytics.module';
|
||||
import { AuthService } from './auth.service';
|
||||
import {
|
||||
PartnerAuthController,
|
||||
ShopAuthController,
|
||||
UserAuthController,
|
||||
UserProfileController,
|
||||
} from './auth.controller';
|
||||
import { UserAddressController } from './user-address.controller';
|
||||
import { UserAddressService } from './user-address.service';
|
||||
import { PartnerStaffController } from './partner-staff.controller';
|
||||
import { PartnerStaffService } from './partner-staff.service';
|
||||
import { StoreStaffController } from './store-staff.controller';
|
||||
import { StoreStaffService } from './store-staff.service';
|
||||
import { AdminAuthController } from './admin-auth.controller';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { PhoneVerifiedGuard } from '../../common/guards/phone-verified.guard';
|
||||
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
||||
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
|
||||
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
|
||||
import { ShopPrimaryGuard } from '../../common/guards/shop-primary.guard';
|
||||
import { StoreMembershipService } from '../../common/guards/store-membership.service';
|
||||
import {
|
||||
HqPermissionGuard,
|
||||
HqPermissionsResolver,
|
||||
} from '../../common/guards/hq-permission.guard';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
IntegrationsModule,
|
||||
forwardRef(() => CommonModule),
|
||||
forwardRef(() => AnalyticsModule),
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET || 'dukang-prev1-dev-secret',
|
||||
signOptions: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
|
||||
}),
|
||||
],
|
||||
controllers: [
|
||||
UserAuthController,
|
||||
ShopAuthController,
|
||||
PartnerAuthController,
|
||||
PartnerStaffController,
|
||||
StoreStaffController,
|
||||
UserProfileController,
|
||||
UserAddressController,
|
||||
AdminAuthController,
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
UserAddressService,
|
||||
PartnerStaffService,
|
||||
StoreStaffService,
|
||||
StoreMembershipService,
|
||||
JwtAuthGuard,
|
||||
PhoneVerifiedGuard,
|
||||
OptionalJwtAuthGuard,
|
||||
HqAuthGuard,
|
||||
PartnerPrimaryGuard,
|
||||
PartnerPermissionGuard,
|
||||
ShopStoreGuard,
|
||||
ShopPrimaryGuard,
|
||||
HqPermissionsResolver,
|
||||
HqPermissionGuard,
|
||||
],
|
||||
exports: [
|
||||
AuthService,
|
||||
UserAddressService,
|
||||
PartnerStaffService,
|
||||
StoreStaffService,
|
||||
StoreMembershipService,
|
||||
JwtModule,
|
||||
JwtAuthGuard,
|
||||
PhoneVerifiedGuard,
|
||||
OptionalJwtAuthGuard,
|
||||
HqAuthGuard,
|
||||
PartnerPrimaryGuard,
|
||||
PartnerPermissionGuard,
|
||||
ShopStoreGuard,
|
||||
ShopPrimaryGuard,
|
||||
HqPermissionsResolver,
|
||||
HqPermissionGuard,
|
||||
],
|
||||
})
|
||||
export class IamModule {}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerStaffService } from './partner-staff.service';
|
||||
import {
|
||||
CreatePartnerStaffDto,
|
||||
SendPartnerStaffPhoneSmsDto,
|
||||
UpdatePartnerStaffDto,
|
||||
} from './dto/partner-staff.dto';
|
||||
|
||||
@Controller('partner/staff')
|
||||
@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
|
||||
export class PartnerStaffController {
|
||||
constructor(private readonly staffService: PartnerStaffService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: AuthUser) {
|
||||
return this.staffService.listStaff(user.actorId);
|
||||
}
|
||||
|
||||
@Post('send-phone-sms')
|
||||
sendPhoneSms(@CurrentUser() user: AuthUser, @Body() dto: SendPartnerStaffPhoneSmsDto) {
|
||||
return this.staffService.sendStaffPhoneSms(user, dto.phone);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@CurrentUser() user: AuthUser, @Body() dto: CreatePartnerStaffDto) {
|
||||
return this.staffService.createStaff(user, dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdatePartnerStaffDto,
|
||||
) {
|
||||
return this.staffService.updateStaff(user, BigInt(id), dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.staffService.deleteStaff(user, BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { ClientApp, DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS, PartnerStaffRole, SmsScene } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { AuthService } from './auth.service';
|
||||
import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PartnerStaffService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly analytics: AnalyticsService,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
async listStaff(parentAccountId: bigint) {
|
||||
const rows = await this.prisma.partnerAccount.findMany({
|
||||
where: { parentAccountId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return rows.map((row) => this.toStaffItem(row));
|
||||
}
|
||||
|
||||
async sendStaffPhoneSms(actor: AuthUser, phone: string) {
|
||||
const parentAccountId = actor.actorId;
|
||||
const parent = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: parentAccountId },
|
||||
});
|
||||
if (parent.isPrimary !== 1) {
|
||||
throw new BadRequestException('仅主账号可添加子账号');
|
||||
}
|
||||
|
||||
const normalized = phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(normalized)) {
|
||||
throw new BadRequestException('请输入正确的手机号码');
|
||||
}
|
||||
|
||||
const existing = await this.prisma.partnerAccount.findUnique({ where: { phone: normalized } });
|
||||
if (existing) throw new BadRequestException('该手机号已被使用');
|
||||
|
||||
const masked = this.maskPhone(normalized);
|
||||
try {
|
||||
await this.authService.sendSms(normalized, SmsScene.PARTNER_STAFF_ADD, {
|
||||
clientApp: ClientApp.PARTNER_H5,
|
||||
});
|
||||
this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, {
|
||||
phone: masked,
|
||||
scene: SmsScene.PARTNER_STAFF_ADD,
|
||||
status: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) {
|
||||
this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_send', parent.id, {
|
||||
phone: masked,
|
||||
scene: SmsScene.PARTNER_STAFF_ADD,
|
||||
status: 'failed',
|
||||
reason: err.message,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return { ok: true, maskedPhone: masked };
|
||||
}
|
||||
|
||||
async createStaff(actor: AuthUser, dto: CreatePartnerStaffDto) {
|
||||
const parentAccountId = actor.actorId;
|
||||
const parent = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: parentAccountId },
|
||||
});
|
||||
if (parent.isPrimary !== 1) {
|
||||
throw new BadRequestException('仅主账号可添加子账号');
|
||||
}
|
||||
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new BadRequestException('请输入正确的手机号码');
|
||||
}
|
||||
|
||||
const existing = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||
if (existing) throw new BadRequestException('该手机号已被使用');
|
||||
|
||||
const smsCode = dto.smsCode.trim();
|
||||
if (!smsCode) throw new BadRequestException('请输入手机号验证码');
|
||||
try {
|
||||
await this.authService.verifySmsCode(phone, smsCode, SmsScene.PARTNER_STAFF_ADD);
|
||||
} catch (err) {
|
||||
this.trackStaffEvent(actor, parent.id, 'partner_staff_sms_verify_fail', parentAccountId, {
|
||||
phone: this.maskPhone(phone),
|
||||
reason: err instanceof BadRequestException ? err.message : '验证码错误',
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||
|
||||
const staffRole = (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL;
|
||||
const permissions =
|
||||
dto.permissions && dto.permissions.length > 0
|
||||
? dto.permissions
|
||||
: [...DEFAULT_PARTNER_STORE_STAFF_PERMISSIONS];
|
||||
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
phone,
|
||||
name,
|
||||
staffRole,
|
||||
permissions,
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
status: 'DISABLED',
|
||||
},
|
||||
});
|
||||
|
||||
this.trackStaffEvent(actor, parent.id, 'partner_staff_create', account.id, {
|
||||
name,
|
||||
phone: this.maskPhone(phone),
|
||||
staffRole,
|
||||
permissions,
|
||||
status: account.status,
|
||||
phoneVerified: true,
|
||||
});
|
||||
|
||||
return this.toStaffItem(account);
|
||||
}
|
||||
|
||||
async updateStaff(actor: AuthUser, staffId: bigint, dto: UpdatePartnerStaffDto) {
|
||||
const parentAccountId = actor.actorId;
|
||||
const staff = await this.assertStaffOwned(parentAccountId, staffId);
|
||||
const before = {
|
||||
name: staff.name,
|
||||
staffRole: staff.staffRole,
|
||||
status: staff.status,
|
||||
};
|
||||
const data: Record<string, unknown> = {};
|
||||
if (dto.name !== undefined) {
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||
data.name = name;
|
||||
}
|
||||
if (dto.staffRole !== undefined) {
|
||||
data.staffRole = dto.staffRole as PartnerStaffRole;
|
||||
}
|
||||
if (dto.permissions !== undefined) {
|
||||
data.permissions = dto.permissions;
|
||||
}
|
||||
if (dto.status !== undefined) {
|
||||
data.status = dto.status;
|
||||
}
|
||||
const updated = await this.prisma.partnerAccount.update({
|
||||
where: { id: staff.id },
|
||||
data,
|
||||
});
|
||||
|
||||
const onlyRoleChange =
|
||||
(dto.staffRole !== undefined || dto.permissions !== undefined) &&
|
||||
dto.name === undefined &&
|
||||
dto.status === undefined;
|
||||
const eventName = onlyRoleChange ? 'partner_staff_permission_update' : 'partner_staff_update';
|
||||
|
||||
const primaryId = parentAccountId;
|
||||
this.trackStaffEvent(actor, primaryId, eventName, staff.id, {
|
||||
before,
|
||||
after: {
|
||||
name: updated.name,
|
||||
staffRole: updated.staffRole,
|
||||
status: updated.status,
|
||||
},
|
||||
});
|
||||
|
||||
return this.toStaffItem(updated);
|
||||
}
|
||||
|
||||
async deleteStaff(actor: AuthUser, staffId: bigint) {
|
||||
const parentAccountId = actor.actorId;
|
||||
const staff = await this.assertStaffOwned(parentAccountId, staffId);
|
||||
|
||||
this.trackStaffEvent(actor, parentAccountId, 'partner_staff_delete', staff.id, {
|
||||
name: staff.name,
|
||||
phone: this.maskPhone(staff.phone),
|
||||
staffRole: staff.staffRole,
|
||||
status: staff.status,
|
||||
});
|
||||
|
||||
await this.prisma.partnerAccount.delete({ where: { id: staff.id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
private trackStaffEvent(
|
||||
actor: AuthUser,
|
||||
primaryAccountId: bigint,
|
||||
eventName: string,
|
||||
refId: bigint,
|
||||
extraJson?: Record<string, unknown>,
|
||||
) {
|
||||
this.analytics.trackPartnerOneSafe(actor.actorId, actor.clientApp, {
|
||||
partnerAccountId: primaryAccountId,
|
||||
eventName,
|
||||
refType: 'PARTNER_ACCOUNT',
|
||||
refId,
|
||||
extraJson,
|
||||
});
|
||||
}
|
||||
|
||||
private async assertStaffOwned(parentAccountId: bigint, staffId: bigint) {
|
||||
const staff = await this.prisma.partnerAccount.findFirst({
|
||||
where: { id: staffId, parentAccountId },
|
||||
});
|
||||
if (!staff) throw new NotFoundException('子账号不存在');
|
||||
return staff;
|
||||
}
|
||||
|
||||
private toStaffItem(row: {
|
||||
id: bigint;
|
||||
name: string;
|
||||
phone: string;
|
||||
staffRole: string | null;
|
||||
permissions?: unknown;
|
||||
status: string;
|
||||
lastLoginAt: Date | null;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id.toString(),
|
||||
name: row.name,
|
||||
phone: this.maskPhone(row.phone),
|
||||
staffRole: row.staffRole ?? PartnerStaffRole.INTERNAL,
|
||||
permissions: Array.isArray(row.permissions) ? row.permissions : undefined,
|
||||
status: row.status,
|
||||
lastLoginAt: row.lastLoginAt?.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
private maskPhone(phone: string): string {
|
||||
if (phone.length !== 11) return phone;
|
||||
return `${phone.slice(0, 3)} **** ${phone.slice(7)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { StoreStaffService } from './store-staff.service';
|
||||
import { CreateStoreStaffDto, UpdateStoreStaffDto } from './dto/store-staff.dto';
|
||||
|
||||
@Controller('shop/staff')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class StoreStaffController {
|
||||
constructor(private readonly staffService: StoreStaffService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: AuthUser) {
|
||||
return this.staffService.listStaff(user.actorId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@CurrentUser() user: AuthUser, @Body() dto: CreateStoreStaffDto) {
|
||||
return this.staffService.createStaff(user, dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateStoreStaffDto,
|
||||
) {
|
||||
return this.staffService.updateStaff(user, BigInt(id), dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.staffService.deleteStaff(user, BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
STORE_STAFF_DEFAULT_PERMISSIONS,
|
||||
StoreStaffRole,
|
||||
} from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { CreateStoreStaffDto, UpdateStoreStaffDto } from './dto/store-staff.dto';
|
||||
|
||||
@Injectable()
|
||||
export class StoreStaffService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly analytics: AnalyticsService,
|
||||
) {}
|
||||
|
||||
async listStaff(parentAccountId: bigint) {
|
||||
const parent = await this.assertPrimary(parentAccountId);
|
||||
const rows = await this.prisma.storeAccount.findMany({
|
||||
where: { parentAccountId: parent.id },
|
||||
include: {
|
||||
bindings: {
|
||||
include: {
|
||||
store: {
|
||||
select: { id: true, name: true, status: true, district: true, address: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return rows.map((row) => this.toStaffItem(row));
|
||||
}
|
||||
|
||||
async createStaff(actor: AuthUser, dto: CreateStoreStaffDto) {
|
||||
const parent = await this.assertPrimary(actor.actorId);
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new BadRequestException('请输入正确的手机号码');
|
||||
}
|
||||
const existing = await this.prisma.storeAccount.findUnique({ where: { phone } });
|
||||
if (existing) throw new BadRequestException('该手机号已被使用');
|
||||
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||
|
||||
const storeIds = await this.resolveOwnedStoreIds(parent.id, dto.storeIds);
|
||||
if (!storeIds.length) throw new BadRequestException('请至少绑定一家门店');
|
||||
|
||||
const staffRole = (dto.staffRole as StoreStaffRole | undefined) ?? StoreStaffRole.CASHIER;
|
||||
const permissions = dto.permissions?.length
|
||||
? dto.permissions
|
||||
: [...STORE_STAFF_DEFAULT_PERMISSIONS];
|
||||
|
||||
const account = await this.prisma.storeAccount.create({
|
||||
data: {
|
||||
phone,
|
||||
name,
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
staffRole,
|
||||
permissions,
|
||||
status: 'ACTIVE',
|
||||
bindings: {
|
||||
create: storeIds.map((storeId) => ({ storeId })),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
bindings: {
|
||||
include: {
|
||||
store: {
|
||||
select: { id: true, name: true, status: true, district: true, address: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.trackStaffEvent(actor, parent.id, 'store_staff_create', account.id, {
|
||||
name,
|
||||
phone: this.maskPhone(phone),
|
||||
staffRole,
|
||||
storeIds: storeIds.map(String),
|
||||
});
|
||||
|
||||
return this.toStaffItem(account);
|
||||
}
|
||||
|
||||
async updateStaff(actor: AuthUser, staffId: bigint, dto: UpdateStoreStaffDto) {
|
||||
const parent = await this.assertPrimary(actor.actorId);
|
||||
const staff = await this.assertStaffOwned(parent.id, staffId);
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
if (dto.name !== undefined) {
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||
data.name = name;
|
||||
}
|
||||
if (dto.staffRole !== undefined) data.staffRole = dto.staffRole;
|
||||
if (dto.permissions !== undefined) data.permissions = dto.permissions;
|
||||
if (dto.status !== undefined) data.status = dto.status;
|
||||
|
||||
if (dto.storeIds !== undefined) {
|
||||
const storeIds = await this.resolveOwnedStoreIds(parent.id, dto.storeIds);
|
||||
if (!storeIds.length) throw new BadRequestException('请至少绑定一家门店');
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.storeAccountStore.deleteMany({ where: { storeAccountId: staff.id } }),
|
||||
this.prisma.storeAccountStore.createMany({
|
||||
data: storeIds.map((storeId) => ({ storeAccountId: staff.id, storeId })),
|
||||
}),
|
||||
this.prisma.storeAccount.update({ where: { id: staff.id }, data }),
|
||||
]);
|
||||
} else if (Object.keys(data).length) {
|
||||
await this.prisma.storeAccount.update({ where: { id: staff.id }, data });
|
||||
}
|
||||
|
||||
const updated = await this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: staff.id },
|
||||
include: {
|
||||
bindings: {
|
||||
include: {
|
||||
store: {
|
||||
select: { id: true, name: true, status: true, district: true, address: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.trackStaffEvent(actor, parent.id, 'store_staff_update', staff.id, {
|
||||
name: updated.name,
|
||||
status: updated.status,
|
||||
staffRole: updated.staffRole,
|
||||
});
|
||||
|
||||
return this.toStaffItem(updated);
|
||||
}
|
||||
|
||||
async deleteStaff(actor: AuthUser, staffId: bigint) {
|
||||
const parent = await this.assertPrimary(actor.actorId);
|
||||
const staff = await this.assertStaffOwned(parent.id, staffId);
|
||||
this.trackStaffEvent(actor, parent.id, 'store_staff_delete', staff.id, {
|
||||
name: staff.name,
|
||||
phone: this.maskPhone(staff.phone),
|
||||
});
|
||||
await this.prisma.storeAccount.delete({ where: { id: staff.id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
private async assertPrimary(accountId: bigint) {
|
||||
const account = await this.prisma.storeAccount.findUnique({ where: { id: accountId } });
|
||||
if (!account) throw new NotFoundException('门店账号不存在');
|
||||
if (account.isPrimary !== 1) {
|
||||
throw new ForbiddenException('仅主账号可管理子账号');
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
private async assertStaffOwned(parentAccountId: bigint, staffId: bigint) {
|
||||
const staff = await this.prisma.storeAccount.findFirst({
|
||||
where: { id: staffId, parentAccountId },
|
||||
});
|
||||
if (!staff) throw new NotFoundException('子账号不存在');
|
||||
return staff;
|
||||
}
|
||||
|
||||
/** Staff may only bind stores that the primary account itself is bound to. */
|
||||
private async resolveOwnedStoreIds(primaryAccountId: bigint, storeIds: string[]) {
|
||||
const unique = [...new Set(storeIds.map((id) => id.trim()).filter(Boolean))];
|
||||
const ids = unique.map((id) => BigInt(id));
|
||||
const owned = await this.prisma.storeAccountStore.findMany({
|
||||
where: { storeAccountId: primaryAccountId, storeId: { in: ids } },
|
||||
select: { storeId: true },
|
||||
});
|
||||
if (owned.length !== ids.length) {
|
||||
throw new BadRequestException('只能绑定主账号已管理的门店');
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private trackStaffEvent(
|
||||
actor: AuthUser,
|
||||
primaryAccountId: bigint,
|
||||
eventName: string,
|
||||
refId: bigint,
|
||||
extraJson?: Record<string, unknown>,
|
||||
) {
|
||||
this.analytics.trackStoreOneSafe(actor.actorId, actor.clientApp, {
|
||||
storeId: actor.storeId,
|
||||
eventName,
|
||||
refType: 'STORE_ACCOUNT',
|
||||
refId,
|
||||
extraJson: { primaryAccountId: primaryAccountId.toString(), ...extraJson },
|
||||
});
|
||||
}
|
||||
|
||||
private toStaffItem(row: {
|
||||
id: bigint;
|
||||
name: string;
|
||||
phone: string;
|
||||
staffRole: string | null;
|
||||
permissions?: unknown;
|
||||
status: string;
|
||||
lastLoginAt: Date | null;
|
||||
bindings: Array<{
|
||||
store: {
|
||||
id: bigint;
|
||||
name: string;
|
||||
status: string;
|
||||
district: string;
|
||||
address: string;
|
||||
};
|
||||
}>;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id.toString(),
|
||||
name: row.name,
|
||||
phone: this.maskPhone(row.phone),
|
||||
staffRole: row.staffRole ?? StoreStaffRole.CASHIER,
|
||||
permissions: Array.isArray(row.permissions) ? row.permissions : undefined,
|
||||
status: row.status,
|
||||
storeIds: row.bindings.map((b) => b.store.id.toString()),
|
||||
stores: row.bindings.map((b) => ({
|
||||
storeId: b.store.id.toString(),
|
||||
name: b.store.name,
|
||||
status: b.store.status,
|
||||
district: b.store.district,
|
||||
address: b.store.address,
|
||||
})),
|
||||
lastLoginAt: row.lastLoginAt?.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
private maskPhone(phone: string): string {
|
||||
if (phone.length !== 11) return phone;
|
||||
return `${phone.slice(0, 3)} **** ${phone.slice(7)}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { UserAddressService } from './user-address.service';
|
||||
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
@Controller('user/addresses')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class UserAddressController {
|
||||
constructor(private readonly addressService: UserAddressService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: AuthUser) {
|
||||
return this.addressService.list(user.actorId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@CurrentUser() user: AuthUser, @Body() body: Record<string, unknown>) {
|
||||
return this.addressService.create(user.actorId, body);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() body: Record<string, unknown>,
|
||||
) {
|
||||
return this.addressService.update(user.actorId, BigInt(id), body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.addressService.remove(user.actorId, BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class UserAddressService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(userId: bigint) {
|
||||
const list = await this.prisma.userAddress.findMany({
|
||||
where: { userId },
|
||||
orderBy: [{ isDefault: 'desc' }, { updatedAt: 'desc' }],
|
||||
});
|
||||
return serializeBigInt(list);
|
||||
}
|
||||
|
||||
async normalizeDefaultAddress(userId: bigint) {
|
||||
const defaults = await this.prisma.userAddress.findMany({
|
||||
where: { userId, isDefault: 1 },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
if (defaults.length <= 1) return;
|
||||
const keep = defaults[0];
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.userAddress.updateMany({ where: { userId }, data: { isDefault: 0 } });
|
||||
await tx.userAddress.update({ where: { id: keep.id }, data: { isDefault: 1 } });
|
||||
});
|
||||
}
|
||||
|
||||
async create(userId: bigint, body: Record<string, unknown>) {
|
||||
const isDefault = body.isDefault ? 1 : 0;
|
||||
const address = await this.prisma.$transaction(async (tx) => {
|
||||
if (isDefault) {
|
||||
await tx.userAddress.updateMany({ where: { userId }, data: { isDefault: 0 } });
|
||||
}
|
||||
return tx.userAddress.create({
|
||||
data: {
|
||||
userId,
|
||||
receiverName: String(body.receiverName),
|
||||
phone: String(body.phone),
|
||||
province: String(body.province),
|
||||
city: String(body.city),
|
||||
district: String(body.district),
|
||||
detail: String(body.detail),
|
||||
isDefault,
|
||||
},
|
||||
});
|
||||
});
|
||||
return serializeBigInt(address);
|
||||
}
|
||||
|
||||
async update(userId: bigint, id: bigint, body: Record<string, unknown>) {
|
||||
const existing = await this.prisma.userAddress.findFirst({ where: { id, userId } });
|
||||
if (!existing) throw new NotFoundException('地址不存在');
|
||||
const address = await this.prisma.$transaction(async (tx) => {
|
||||
if (body.isDefault) {
|
||||
await tx.userAddress.updateMany({ where: { userId }, data: { isDefault: 0 } });
|
||||
}
|
||||
return tx.userAddress.update({
|
||||
where: { id },
|
||||
data: {
|
||||
receiverName: body.receiverName ? String(body.receiverName) : undefined,
|
||||
phone: body.phone ? String(body.phone) : undefined,
|
||||
province: body.province ? String(body.province) : undefined,
|
||||
city: body.city ? String(body.city) : undefined,
|
||||
district: body.district ? String(body.district) : undefined,
|
||||
detail: body.detail ? String(body.detail) : undefined,
|
||||
isDefault: body.isDefault ? 1 : undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
return serializeBigInt(address);
|
||||
}
|
||||
|
||||
async remove(userId: bigint, id: bigint) {
|
||||
const existing = await this.prisma.userAddress.findFirst({ where: { id, userId } });
|
||||
if (!existing) throw new NotFoundException('地址不存在');
|
||||
await this.prisma.userAddress.delete({ where: { id } });
|
||||
return { deleted: true };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user