feat: 迭代

This commit is contained in:
2026-05-27 18:58:39 +08:00
parent 716a55744e
commit 9baf5f29f7
44 changed files with 928 additions and 2903 deletions
@@ -1,4 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, VersionColumn } from 'typeorm';
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, VersionColumn, ManyToOne, JoinColumn } from 'typeorm';
import { Merchant } from './merchant.entity';
@Entity('merchant_accounts')
@Index(['merchant_id'], { unique: true })
@@ -9,6 +10,10 @@ export class MerchantAccount {
@Column({ type: 'bigint', unsigned: true, comment: '商家ID' })
merchant_id: number;
@ManyToOne(() => Merchant)
@JoinColumn({ name: 'merchant_id' })
merchant: Merchant;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额' })
balance: number;
@@ -30,7 +30,7 @@ export class PlatformWithdrawal {
@Column({ type: 'varchar', length: 50, name: 'bank_account', comment: '银行账号' })
bankAccount: string;
@Column({ type: 'varchar', length: 50, name: 'account_name', comment: '账户名' })
@Column({ type: 'varchar', length: 50, nullable: true, name: 'account_name', comment: '账户名' })
accountName: string;
@Column({ type: 'enum', enum: ['pending', 'approved', 'rejected', 'paid', 'failed'], default: 'pending', comment: '状态' })
@@ -1,4 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, VersionColumn } from 'typeorm';
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, VersionColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('user_accounts')
@Index(['user_id'], { unique: true })
@@ -9,6 +10,10 @@ export class UserAccount {
@Column({ type: 'bigint', unsigned: true, comment: '用户ID' })
user_id: number;
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0, comment: '可用余额' })
balance: number;
@@ -14,6 +14,7 @@ import { UserCouponModule } from './coupon/coupon.module';
import { UserFinanceModule } from './finance/finance.module';
import { UserActivityModule } from './activity/activity.module';
import { RoomModule } from './room/room.module';
import { LocationModule } from './location/location.module';
import { MerchantController } from './merchant/merchant.controller';
import { MerchantService } from '@/modules/merchant/merchant.service';
@@ -29,6 +30,7 @@ import { MerchantService } from '@/modules/merchant/merchant.service';
UserFinanceModule,
UserActivityModule,
RoomModule,
LocationModule,
],
controllers: [MerchantController],
providers: [MerchantService],
@@ -0,0 +1,27 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
import { LocationService } from './location.service';
@ApiTags('位置服务')
@Controller('app/location')
export class LocationController {
constructor(private readonly locationService: LocationService) {}
@Get('geocode')
@ApiOperation({ summary: '逆地理编码:根据经纬度获取城市名' })
@ApiQuery({ name: 'latitude', description: '纬度', example: 31.230416 })
@ApiQuery({ name: 'longitude', description: '经度', example: 121.473701 })
async reverseGeocode(
@Query('latitude') latitude: string,
@Query('longitude') longitude: string,
) {
const lat = parseFloat(latitude);
const lng = parseFloat(longitude);
if (isNaN(lat) || isNaN(lng)) {
return { city: '上海', province: '上海市' };
}
return this.locationService.reverseGeocode(lat, lng);
}
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { LocationController } from './location.controller';
import { LocationService } from './location.service';
@Module({
controllers: [LocationController],
providers: [LocationService],
exports: [LocationService],
})
export class LocationModule {}
@@ -0,0 +1,138 @@
import { Injectable, Logger } from '@nestjs/common';
interface CityBoundary {
name: string;
province: string;
minLat: number;
maxLat: number;
minLng: number;
maxLng: number;
centerLat: number;
centerLng: number;
}
@Injectable()
export class LocationService {
private readonly logger = new Logger(LocationService.name);
// 主要城市的经纬度范围(简化版)
private readonly cityBoundaries: CityBoundary[] = [
// 直辖市
{ name: '北京', province: '北京市', minLat: 39.4, maxLat: 41.1, minLng: 115.4, maxLng: 117.5, centerLat: 39.9042, centerLng: 116.4074 },
{ name: '上海', province: '上海市', minLat: 30.7, maxLat: 31.9, minLng: 120.9, maxLng: 122.0, centerLat: 31.2304, centerLng: 121.4737 },
{ name: '天津', province: '天津市', minLat: 38.6, maxLat: 40.3, minLng: 116.7, maxLng: 118.1, centerLat: 39.3434, centerLng: 117.3616 },
{ name: '重庆', province: '重庆市', minLat: 28.2, maxLat: 32.2, minLng: 105.3, maxLng: 110.2, centerLat: 29.5630, centerLng: 106.5516 },
// 省会城市
{ name: '广州', province: '广东省', minLat: 22.5, maxLat: 23.9, minLng: 112.9, maxLng: 114.0, centerLat: 23.1291, centerLng: 113.2644 },
{ name: '深圳', province: '广东省', minLat: 22.4, maxLat: 22.9, minLng: 113.7, maxLng: 114.6, centerLat: 22.5431, centerLng: 114.0579 },
{ name: '杭州', province: '浙江省', minLat: 29.2, maxLat: 30.6, minLng: 118.3, maxLng: 120.9, centerLat: 30.2741, centerLng: 120.1551 },
{ name: '成都', province: '四川省', minLat: 30.1, maxLat: 31.4, minLng: 102.9, maxLng: 104.9, centerLat: 30.5728, centerLng: 104.0668 },
{ name: '武汉', province: '湖北省', minLat: 29.9, maxLat: 31.4, minLng: 113.7, maxLng: 115.1, centerLat: 30.5928, centerLng: 114.3055 },
{ name: '西安', province: '陕西省', minLat: 33.7, maxLat: 34.8, minLng: 107.4, maxLng: 109.8, centerLat: 34.3416, centerLng: 108.9398 },
{ name: '南京', province: '江苏省', minLat: 31.2, maxLat: 32.6, minLng: 118.4, maxLng: 119.2, centerLat: 32.0603, centerLng: 118.7969 },
{ name: '苏州', province: '江苏省', minLat: 30.8, maxLat: 32.0, minLng: 119.5, maxLng: 121.4, centerLat: 31.2989, centerLng: 120.5853 },
{ name: '长沙', province: '湖南省', minLat: 27.8, maxLat: 28.6, minLng: 112.0, maxLng: 114.3, centerLat: 28.2282, centerLng: 112.9388 },
{ name: '郑州', province: '河南省', minLat: 34.2, maxLat: 35.0, minLng: 112.7, maxLng: 114.1, centerLat: 34.7466, centerLng: 113.6254 },
{ name: '济南', province: '山东省', minLat: 36.0, maxLat: 37.0, minLng: 116.2, maxLng: 117.7, centerLat: 36.6512, centerLng: 117.1205 },
{ name: '青岛', province: '山东省', minLat: 35.4, maxLat: 36.5, minLng: 119.4, maxLng: 121.0, centerLat: 36.0671, centerLng: 120.3826 },
{ name: '厦门', province: '福建省', minLat: 24.2, maxLat: 24.7, minLng: 117.9, maxLng: 118.3, centerLat: 24.4798, centerLng: 118.0894 },
{ name: '福州', province: '福建省', minLat: 25.2, maxLat: 26.5, minLng: 118.3, maxLng: 120.0, centerLat: 26.0745, centerLng: 119.2965 },
{ name: '昆明', province: '云南省', minLat: 24.4, maxLat: 26.3, minLng: 102.1, maxLng: 103.4, centerLat: 25.0406, centerLng: 102.7123 },
{ name: '贵阳', province: '贵州省', minLat: 26.1, maxLat: 27.0, minLng: 106.1, maxLng: 107.2, centerLat: 26.6470, centerLng: 106.6302 },
{ name: '南昌', province: '江西省', minLat: 28.2, maxLat: 29.2, minLng: 115.5, maxLng: 116.3, centerLat: 28.6829, centerLng: 115.8579 },
{ name: '合肥', province: '安徽省', minLat: 31.1, maxLat: 32.2, minLng: 116.8, maxLng: 117.9, centerLat: 31.8206, centerLng: 117.2272 },
{ name: '太原', province: '山西省', minLat: 37.3, maxLat: 38.3, minLng: 111.9, maxLng: 113.1, centerLat: 37.8706, centerLng: 112.5489 },
{ name: '石家庄', province: '河北省', minLat: 37.3, maxLat: 38.7, minLng: 113.5, maxLng: 115.5, centerLat: 38.0428, centerLng: 114.5149 },
{ name: '沈阳', province: '辽宁省', minLat: 41.1, maxLat: 42.2, minLng: 122.3, maxLng: 123.7, centerLat: 41.8057, centerLng: 123.4328 },
{ name: '大连', province: '辽宁省', minLat: 38.8, maxLat: 40.0, minLng: 120.6, maxLng: 123.0, centerLat: 38.9140, centerLng: 121.6147 },
{ name: '哈尔滨', province: '黑龙江省', minLat: 44.0, maxLat: 46.5, minLng: 125.4, maxLng: 130.6, centerLat: 45.8038, centerLng: 126.5340 },
{ name: '长春', province: '吉林省', minLat: 43.1, maxLat: 44.5, minLng: 124.7, maxLng: 126.2, centerLat: 43.8171, centerLng: 125.3235 },
{ name: '兰州', province: '甘肃省', minLat: 35.5, maxLat: 37.0, minLng: 102.6, maxLng: 104.8, centerLat: 36.0611, centerLng: 103.8343 },
{ name: '银川', province: '宁夏回族自治区', minLat: 37.5, maxLat: 39.0, minLng: 105.5, maxLng: 107.0, centerLat: 38.4872, centerLng: 106.2309 },
{ name: '西宁', province: '青海省', minLat: 36.0, maxLat: 37.5, minLng: 100.5, maxLng: 102.5, centerLat: 36.6171, centerLng: 101.7782 },
{ name: '乌鲁木齐', province: '新疆维吾尔自治区', minLat: 42.8, maxLat: 44.5, minLng: 86.5, maxLng: 88.5, centerLat: 43.8256, centerLng: 87.6168 },
{ name: '呼和浩特', province: '内蒙古自治区', minLat: 40.0, maxLat: 41.5, minLng: 110.5, maxLng: 112.5, centerLat: 40.8415, centerLng: 111.7519 },
{ name: '南宁', province: '广西壮族自治区', minLat: 22.1, maxLat: 23.9, minLng: 107.5, maxLng: 109.0, centerLat: 22.8170, centerLng: 108.3665 },
{ name: '拉萨', province: '西藏自治区', minLat: 29.1, maxLat: 30.5, minLng: 90.0, maxLng: 92.0, centerLat: 29.6470, centerLng: 91.1175 },
{ name: '海口', province: '海南省', minLat: 19.5, maxLat: 20.3, minLng: 110.0, maxLng: 110.7, centerLat: 20.0444, centerLng: 110.1999 },
{ name: '三亚', province: '海南省', minLat: 18.2, maxLat: 18.6, minLng: 108.9, maxLng: 109.7, centerLat: 18.2528, centerLng: 109.5122 },
];
/**
* 逆地理编码:将经纬度转换为城市名
* 使用简单的边界匹配算法
*/
async reverseGeocode(latitude: number, longitude: number): Promise<{ city: string; province: string }> {
try {
// 1. 先尝试精确匹配:查找经纬度在城市边界内的
for (const city of this.cityBoundaries) {
if (
latitude >= city.minLat &&
latitude <= city.maxLat &&
longitude >= city.minLng &&
longitude <= city.maxLng
) {
this.logger.log(`精确匹配到城市: ${city.province} ${city.name}`);
return { city: city.name, province: city.province };
}
}
// 2. 如果没有精确匹配,找最近的城市(计算距离)
let nearestCity = this.cityBoundaries[0];
let minDistance = this.calculateDistance(
latitude,
longitude,
nearestCity.centerLat,
nearestCity.centerLng,
);
for (const city of this.cityBoundaries) {
const distance = this.calculateDistance(
latitude,
longitude,
city.centerLat,
city.centerLng,
);
if (distance < minDistance) {
minDistance = distance;
nearestCity = city;
}
}
this.logger.log(`最近城市匹配: ${nearestCity.province} ${nearestCity.name} (距离: ${minDistance.toFixed(2)}km)`);
return { city: nearestCity.name, province: nearestCity.province };
} catch (error) {
this.logger.error(`逆地理编码失败: ${error.message}`);
return { city: '上海', province: '上海市' };
}
}
/**
* 计算两个经纬度之间的距离(单位:公里)
* 使用 Haversine 公式
*/
private calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 6371; // 地球半径(公里)
const dLat = this.toRadians(lat2 - lat1);
const dLng = this.toRadians(lng2 - lng1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRadians(lat1)) *
Math.cos(this.toRadians(lat2)) *
Math.sin(dLng / 2) *
Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* 角度转弧度
*/
private toRadians(degrees: number): number {
return degrees * (Math.PI / 180);
}
}
@@ -620,14 +620,22 @@ export class AccountService {
const amountNum = Number(amount);
const balanceBefore = Number(account.balance);
const frozenBefore = Number(account.frozen_balance);
if (balanceBefore < amountNum) {
throw new BadRequestException('平台账户余额不足');
}
const balanceAfter = parseFloat((balanceBefore - amountNum).toFixed(2));
if (frozenBefore < amountNum) {
throw new BadRequestException('冻结金额不足');
}
const balanceAfter = parseFloat((balanceBefore - amountNum).toFixed(2));
const frozenAfter = parseFloat((frozenBefore - amountNum).toFixed(2));
// 确认打款时:扣减余额和冻结金额
account.balance = balanceAfter;
account.frozen_balance = frozenAfter;
account.version += 1;
await queryRunner.manager.save(account);
@@ -709,7 +717,13 @@ export class AccountService {
take: dto.pageSize ?? 10,
});
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
// 映射数据,添加用户名
const mappedList = list.map(account => ({
...account,
userName: account.user?.nickname || account.user?.phone || '-',
}));
return { list: mappedList, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
}
/**
@@ -718,7 +732,6 @@ export class AccountService {
async getUserAccountDetail(userId: number) {
const account = await this.userAccountRepo.findOne({
where: { user_id: userId },
relations: ['user'],
});
if (!account) {
throw new NotFoundException('用户账户不存在');
@@ -764,7 +777,13 @@ export class AccountService {
take: dto.pageSize ?? 10,
});
return { list, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
// 映射数据,添加商家名称
const mappedList = list.map(account => ({
...account,
merchantName: account.merchant?.shopName || '-',
}));
return { list: mappedList, total, page: dto.page ?? 1, pageSize: dto.pageSize ?? 10 };
}
/**
@@ -773,7 +792,6 @@ export class AccountService {
async getMerchantAccountDetail(merchantId: number) {
const account = await this.merchantAccountRepo.findOne({
where: { merchant_id: merchantId },
relations: ['merchant'],
});
if (!account) {
throw new NotFoundException('商家账户不存在');
@@ -91,9 +91,10 @@ export class CreatePlatformWithdrawalDto {
@IsString()
bankAccount: string;
@ApiProperty({ description: '账户名', example: '某某公司' })
@ApiPropertyOptional({ description: '账户名', example: '某某公司' })
@IsOptional()
@IsString()
accountName: string;
accountName?: string;
}
export class QueryPlatformWithdrawalDto {
@@ -403,7 +403,7 @@ export class ReportService {
'o.status',
'o.createdAt',
'm.id',
'm.name',
'm.shopName',
'u.id',
'u.nickname',
's.settledAt',
@@ -145,7 +145,7 @@ export class WithdrawalService {
amount: number;
bankName: string;
bankAccount: string;
accountName: string;
accountName?: string;
}) {
const { amount, bankName, bankAccount, accountName } = dto;
@@ -155,19 +155,13 @@ export class WithdrawalService {
const account = await this.accountService.getPlatformAccount();
// 计算可提现金额 = 累计收入 - 冻结余额
// 注意:这里使用 total_income 是为了确保只提现平台收入
// 未来如果有其他收入(广告费、会员费等),需要累加到可提现金额中
const availableAmount = Number(account.total_income) - Number(account.frozen_balance);
// 计算可提现金额 = 钱包余额 - 冻结余额
const availableAmount = Number(account.balance) - Number(account.frozen_balance);
if (availableAmount < amount) {
throw new BadRequestException(`可提现金额不足,当前可提现:${availableAmount.toFixed(2)}`);
}
if (Number(account.balance) < amount) {
throw new BadRequestException('账户余额不足');
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
@@ -178,11 +172,16 @@ export class WithdrawalService {
lock: { mode: 'pessimistic_write' }
});
if (!lockedAccount || Number(lockedAccount.balance) < amount) {
throw new BadRequestException('账户余额不足');
if (!lockedAccount) {
throw new BadRequestException('账户不存在');
}
lockedAccount.balance = Number(lockedAccount.balance) - amount;
const currentAvailable = Number(lockedAccount.balance) - Number(lockedAccount.frozen_balance);
if (currentAvailable < amount) {
throw new BadRequestException('可提现金额不足');
}
// 申请提现时:只增加冻结金额,不扣减余额
lockedAccount.frozen_balance = Number(lockedAccount.frozen_balance) + amount;
lockedAccount.version += 1;