城市合伙人端的修改(后台)
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
# 服务器:仅使用 .env.production(bash deploy/sync-api-env.sh production)
|
||||
# .env.development — 本地参考模板,不部署到服务器
|
||||
|
||||
DATABASE_URL="mysql://root:root@localhost:3306/dukang_haoke"
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
DATABASE_URL="mysql://root:root@localhost:6016/dukang_haoke"
|
||||
REDIS_URL="redis://localhost:6017"
|
||||
JWT_SECRET="dukang-prev1-dev-secret-change-in-prod"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
PORT=3000
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:validate": "prisma validate",
|
||||
"prisma:seed": "ts-node --transpile-only prisma/seed-v31.ts",
|
||||
"prisma:migrate-city-partner": "ts-node --transpile-only prisma/migrate-city-partner.ts",
|
||||
"prisma:seed-legacy": "ts-node --transpile-only prisma/seed-prev1.ts",
|
||||
"prisma:upsert-super-admin": "ts-node --transpile-only scripts/upsert-super-admin.ts",
|
||||
"prisma:sync-benefit": "ts-node --transpile-only prisma/sync-benefit-to-price.ts"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/** 清空开城城市及关联数据(保留 HQ 账户、商品目录等) */
|
||||
async function main() {
|
||||
await prisma.logPartnerAnalytics.deleteMany();
|
||||
await prisma.redeemRecord.deleteMany();
|
||||
await prisma.benefitCoupon.deleteMany();
|
||||
await prisma.orderDelivery.deleteMany();
|
||||
await prisma.order.deleteMany();
|
||||
await prisma.commonEvent.deleteMany({ where: { refType: { in: ['CITY', 'WAREHOUSE', 'PARTNER', 'PARTNER_ACCOUNT', 'STORE'] } } });
|
||||
await prisma.storeAccount.deleteMany();
|
||||
await prisma.store.deleteMany();
|
||||
await prisma.partnerBill.deleteMany();
|
||||
await prisma.cityWarehouse.deleteMany();
|
||||
await prisma.partnerAccount.deleteMany();
|
||||
const result = await prisma.commonCity.deleteMany();
|
||||
console.log(`Cleared ${result.count} cities and related open-city data.`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -148,25 +148,7 @@ CREATE TABLE common_promo_code (
|
||||
UNIQUE KEY uk_common_promo_code_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='推广码';
|
||||
|
||||
-- ===================== PARTNER(先于 city/store) =============================
|
||||
|
||||
DROP TABLE IF EXISTS partner_partner;
|
||||
CREATE TABLE partner_partner (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
company_name VARCHAR(128) NOT NULL,
|
||||
address VARCHAR(256) NOT NULL,
|
||||
contact_phone VARCHAR(20) NOT NULL,
|
||||
contract_no VARCHAR(64) DEFAULT NULL COMMENT '合同编号',
|
||||
contract_signed_at DATETIME(3) DEFAULT NULL,
|
||||
contract_expire_at DATETIME(3) DEFAULT NULL,
|
||||
bank_account_name VARCHAR(64) DEFAULT NULL,
|
||||
bank_account_no VARCHAR(32) DEFAULT NULL,
|
||||
bank_branch VARCHAR(128) DEFAULT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_partner_partner_phone (contact_phone)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='城市合伙人主体';
|
||||
-- ===================== PARTNER(城市合伙人主账号 + 子账号) =============================
|
||||
|
||||
DROP TABLE IF EXISTS common_city;
|
||||
CREATE TABLE common_city (
|
||||
@@ -175,71 +157,99 @@ CREATE TABLE common_city (
|
||||
name VARCHAR(64) NOT NULL,
|
||||
province VARCHAR(32) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'PENDING',
|
||||
partner_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
local_min_qty INT NOT NULL DEFAULT 2,
|
||||
cross_min_qty INT NOT NULL DEFAULT 6,
|
||||
max_partner_commission_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0500 COMMENT '订单+核销佣金合计上限',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_common_city_code (code),
|
||||
KEY idx_common_city_partner (partner_id),
|
||||
CONSTRAINT fk_common_city_partner FOREIGN KEY (partner_id) REFERENCES partner_partner(id) ON DELETE SET NULL
|
||||
UNIQUE KEY uk_common_city_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='开城配置';
|
||||
|
||||
DROP TABLE IF EXISTS common_city_commission_rule;
|
||||
CREATE TABLE common_city_commission_rule (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
city_id BIGINT UNSIGNED NOT NULL,
|
||||
order_commission_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0000,
|
||||
redeem_commission_rate DECIMAL(5,4) NOT NULL DEFAULT 0.0000,
|
||||
partner_profit_rate DECIMAL(5,4) NOT NULL DEFAULT 0.3500,
|
||||
store_settlement_rate DECIMAL(5,4) NOT NULL DEFAULT 0.6000,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_common_city_commission_city (city_id),
|
||||
CONSTRAINT fk_common_city_commission_city FOREIGN KEY (city_id) REFERENCES common_city(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='城市佣金规则';
|
||||
|
||||
DROP TABLE IF EXISTS partner_account;
|
||||
CREATE TABLE partner_account (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
partner_id BIGINT UNSIGNED NOT NULL,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
wx_open_id VARCHAR(64) DEFAULT NULL,
|
||||
wx_union_id VARCHAR(64) DEFAULT NULL,
|
||||
is_primary TINYINT NOT NULL DEFAULT 0,
|
||||
parent_account_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
staff_role VARCHAR(16) DEFAULT NULL COMMENT 'PARTNER|INTERNAL|PROMOTER',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
|
||||
last_login_at DATETIME(3) DEFAULT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
wx_open_id VARCHAR(64) DEFAULT NULL,
|
||||
wx_union_id VARCHAR(64) DEFAULT NULL,
|
||||
is_primary TINYINT NOT NULL DEFAULT 0,
|
||||
parent_account_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
staff_role VARCHAR(16) DEFAULT NULL COMMENT 'PARTNER|INTERNAL|PROMOTER',
|
||||
permissions JSON DEFAULT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
|
||||
last_login_at DATETIME(3) DEFAULT NULL,
|
||||
city_id BIGINT UNSIGNED DEFAULT NULL COMMENT '主账号绑定城市',
|
||||
scope_type VARCHAR(16) DEFAULT NULL COMMENT 'CITY_WIDE|DISTRICT',
|
||||
district_codes JSON DEFAULT NULL,
|
||||
order_commission_rate DECIMAL(5,4) DEFAULT 0.0000,
|
||||
redeem_commission_rate DECIMAL(5,4) DEFAULT 0.0300,
|
||||
binding_status VARCHAR(16) DEFAULT 'ACTIVE',
|
||||
company_name VARCHAR(128) DEFAULT NULL,
|
||||
address VARCHAR(256) DEFAULT NULL,
|
||||
contact_phone VARCHAR(20) DEFAULT NULL,
|
||||
contract_no VARCHAR(64) DEFAULT NULL,
|
||||
contract_signed_at DATETIME(3) DEFAULT NULL,
|
||||
contract_expire_at DATETIME(3) DEFAULT NULL,
|
||||
bank_account_name VARCHAR(64) DEFAULT NULL,
|
||||
bank_account_no VARCHAR(32) DEFAULT NULL,
|
||||
bank_branch VARCHAR(128) DEFAULT NULL,
|
||||
weekly_store_target INT DEFAULT 20,
|
||||
managed_warehouse_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_partner_account_phone (phone),
|
||||
KEY idx_partner_account_partner (partner_id),
|
||||
CONSTRAINT fk_partner_account_partner FOREIGN KEY (partner_id) REFERENCES partner_partner(id) ON DELETE RESTRICT,
|
||||
UNIQUE KEY uk_partner_account_managed_warehouse (managed_warehouse_id),
|
||||
KEY idx_partner_account_city_scope (city_id, scope_type),
|
||||
KEY idx_partner_account_city_primary (city_id, is_primary),
|
||||
KEY idx_partner_account_parent (parent_account_id),
|
||||
KEY idx_partner_account_contact_phone (contact_phone),
|
||||
CONSTRAINT fk_partner_account_city FOREIGN KEY (city_id) REFERENCES common_city(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_partner_account_parent FOREIGN KEY (parent_account_id) REFERENCES partner_account(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合伙人账号';
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='城市合伙人账号(主账号=实体+城市绑定)';
|
||||
|
||||
DROP TABLE IF EXISTS common_city_warehouse;
|
||||
CREATE TABLE common_city_warehouse (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
city_id BIGINT UNSIGNED NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
address VARCHAR(256) NOT NULL,
|
||||
contact_name VARCHAR(64) NOT NULL,
|
||||
contact_phone VARCHAR(20) NOT NULL,
|
||||
manager_type VARCHAR(16) NOT NULL COMMENT 'HQ|PARTNER',
|
||||
partner_account_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_city_warehouse_city (city_id),
|
||||
KEY idx_city_warehouse_partner_account (partner_account_id),
|
||||
CONSTRAINT fk_city_warehouse_city FOREIGN KEY (city_id) REFERENCES common_city(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_city_warehouse_partner_account FOREIGN KEY (partner_account_id) REFERENCES partner_account(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='城市仓库';
|
||||
|
||||
ALTER TABLE partner_account
|
||||
ADD CONSTRAINT fk_partner_account_managed_warehouse FOREIGN KEY (managed_warehouse_id) REFERENCES common_city_warehouse(id) ON DELETE SET NULL;
|
||||
|
||||
DROP TABLE IF EXISTS partner_bill;
|
||||
CREATE TABLE partner_bill (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
bill_no VARCHAR(32) NOT NULL,
|
||||
partner_id BIGINT UNSIGNED NOT NULL,
|
||||
period_start DATETIME(3) NOT NULL,
|
||||
period_end DATETIME(3) NOT NULL,
|
||||
order_commission DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '下单佣金汇总',
|
||||
redeem_commission DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '核销佣金汇总',
|
||||
total_amount DECIMAL(10,2) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'DRAFT',
|
||||
confirmed_at DATETIME(3) DEFAULT NULL,
|
||||
paid_at DATETIME(3) DEFAULT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
bill_no VARCHAR(32) NOT NULL,
|
||||
partner_account_id BIGINT UNSIGNED NOT NULL,
|
||||
period_start DATETIME(3) NOT NULL,
|
||||
period_end DATETIME(3) NOT NULL,
|
||||
order_commission DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '下单佣金汇总',
|
||||
redeem_commission DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '核销佣金汇总',
|
||||
total_amount DECIMAL(10,2) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'DRAFT',
|
||||
confirmed_at DATETIME(3) DEFAULT NULL,
|
||||
paid_at DATETIME(3) DEFAULT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_partner_bill_no (bill_no),
|
||||
KEY idx_partner_bill_partner_status (partner_id, status),
|
||||
CONSTRAINT fk_partner_bill_partner FOREIGN KEY (partner_id) REFERENCES partner_partner(id) ON DELETE RESTRICT
|
||||
KEY idx_partner_bill_account_status (partner_account_id, status),
|
||||
CONSTRAINT fk_partner_bill_account FOREIGN KEY (partner_account_id) REFERENCES partner_account(id) ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合伙人T+30账单';
|
||||
|
||||
-- ===================== HQ =============================
|
||||
@@ -345,7 +355,7 @@ DROP TABLE IF EXISTS store_store;
|
||||
CREATE TABLE store_store (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
city_id BIGINT UNSIGNED NOT NULL,
|
||||
partner_id BIGINT UNSIGNED NOT NULL,
|
||||
partner_account_id BIGINT UNSIGNED NOT NULL,
|
||||
category_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
@@ -366,13 +376,14 @@ CREATE TABLE store_store (
|
||||
bank_account_name VARCHAR(64) DEFAULT NULL,
|
||||
bank_account_no VARCHAR(32) DEFAULT NULL,
|
||||
bank_branch VARCHAR(128) DEFAULT NULL,
|
||||
settlement_rate DECIMAL(5,4) NOT NULL DEFAULT 0.6000 COMMENT '门店核销结算比例',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_store_store_city_status (city_id, status),
|
||||
KEY idx_store_store_partner (partner_id),
|
||||
KEY idx_store_store_partner_account (partner_account_id),
|
||||
CONSTRAINT fk_store_store_city FOREIGN KEY (city_id) REFERENCES common_city(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_store_store_partner FOREIGN KEY (partner_id) REFERENCES partner_partner(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_store_store_partner_account FOREIGN KEY (partner_account_id) REFERENCES partner_account(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_store_store_category FOREIGN KEY (category_id) REFERENCES common_store_category(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='餐饮门店';
|
||||
|
||||
@@ -446,7 +457,9 @@ CREATE TABLE user_order (
|
||||
shipped_at DATETIME(3) DEFAULT NULL COMMENT '发货时间(冗余=user_order_delivery.shipping_at)',
|
||||
completed_at DATETIME(3) DEFAULT NULL COMMENT '完成时间',
|
||||
cancelled_at DATETIME(3) DEFAULT NULL COMMENT '取消时间',
|
||||
pay_expire_at DATETIME(3) DEFAULT NULL COMMENT '待付款过期时间',
|
||||
pay_expire_at DATETIME(3) DEFAULT NULL COMMENT '待付款过期时间',
|
||||
partner_account_id_at_pay BIGINT UNSIGNED DEFAULT NULL,
|
||||
order_commission_rate_at_pay DECIMAL(5,4) DEFAULT NULL,
|
||||
remark VARCHAR(512) DEFAULT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 将 legacy common_city.partner_id 迁移至 common_city_partner(CITY_WIDE)。
|
||||
* 在 prisma db push 移除 partner_id 列之前运行;若列已不存在则跳过数据拷贝。
|
||||
*
|
||||
* 用法:cd server/dukang-api && pnpm prisma:migrate-city-partner
|
||||
*/
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function columnExists(table: string, column: string): Promise<boolean> {
|
||||
const rows = await prisma.$queryRawUnsafe<Array<{ cnt: bigint }>>(
|
||||
`SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
|
||||
table,
|
||||
column,
|
||||
);
|
||||
return Number(rows[0]?.cnt ?? 0) > 0;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const hasPartnerId = await columnExists('common_city', 'partner_id');
|
||||
if (!hasPartnerId) {
|
||||
console.log('[migrate-city-partner] common_city.partner_id 已移除,跳过迁移');
|
||||
return;
|
||||
}
|
||||
|
||||
const cities = await prisma.$queryRawUnsafe<
|
||||
Array<{ id: bigint; partner_id: bigint | null }>
|
||||
>(`SELECT id, partner_id FROM common_city WHERE partner_id IS NOT NULL`);
|
||||
|
||||
let migrated = 0;
|
||||
for (const row of cities) {
|
||||
const cityId = row.id;
|
||||
const partnerId = row.partner_id!;
|
||||
|
||||
const existing = await prisma.cityPartner.findFirst({
|
||||
where: { cityId, partnerId },
|
||||
});
|
||||
if (existing) continue;
|
||||
|
||||
const rule = await prisma.commonCityCommissionRule.findUnique({ where: { cityId } });
|
||||
await prisma.cityPartner.create({
|
||||
data: {
|
||||
cityId,
|
||||
partnerId,
|
||||
scopeType: 'CITY_WIDE',
|
||||
orderCommissionRate: rule ? Number((rule as { orderCommissionRate?: unknown }).orderCommissionRate ?? 0) : 0,
|
||||
redeemCommissionRate: rule ? Number((rule as { redeemCommissionRate?: unknown }).redeemCommissionRate ?? 0.03) : 0.03,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
migrated += 1;
|
||||
}
|
||||
|
||||
console.log(`[migrate-city-partner] 已迁移 ${migrated} 条 CITY_WIDE 绑定`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Partner architecture unification migration stub.
|
||||
*
|
||||
* Schema change: Partner / CityPartner / CommonCityCommissionRule removed;
|
||||
* PartnerAccount is the primary entity; Store.partnerAccountId;
|
||||
* Order.partnerAccountIdAtPay.
|
||||
*
|
||||
* This is a breaking schema change — there is no incremental SQL migration path
|
||||
* from the legacy v3.1 tables. Apply on a fresh or disposable database:
|
||||
*
|
||||
* cd server/dukang-api
|
||||
* npx prisma db push --force-reset
|
||||
* pnpm prisma:seed
|
||||
*
|
||||
* Do NOT run --force-reset against production.
|
||||
*/
|
||||
|
||||
async function main() {
|
||||
console.log(`
|
||||
Partner account unification requires a full schema reset.
|
||||
|
||||
Run:
|
||||
cd server/dukang-api
|
||||
npx prisma db push --force-reset
|
||||
pnpm prisma:seed
|
||||
`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -106,6 +106,26 @@ enum CityStatus {
|
||||
PAUSED
|
||||
}
|
||||
|
||||
enum CityPartnerScopeType {
|
||||
CITY_WIDE
|
||||
DISTRICT
|
||||
}
|
||||
|
||||
enum CityPartnerStatus {
|
||||
ACTIVE
|
||||
PAUSED
|
||||
}
|
||||
|
||||
enum WarehouseManagerType {
|
||||
HQ
|
||||
PARTNER
|
||||
}
|
||||
|
||||
enum WarehouseStatus {
|
||||
ACTIVE
|
||||
PAUSED
|
||||
}
|
||||
|
||||
enum PartnerStaffRole {
|
||||
PARTNER
|
||||
INTERNAL
|
||||
@@ -391,103 +411,109 @@ model CommonCity {
|
||||
name String @db.VarChar(64)
|
||||
province String @db.VarChar(32)
|
||||
status CityStatus @default(PENDING)
|
||||
partnerId BigInt? @map("partner_id") @db.UnsignedBigInt
|
||||
localMinQty Int @default(2) @map("local_min_qty")
|
||||
crossMinQty Int @default(6) @map("cross_min_qty")
|
||||
maxPartnerCommissionRate Decimal @default(0.05) @map("max_partner_commission_rate") @db.Decimal(5, 4)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
partner Partner? @relation(fields: [partnerId], references: [id], onDelete: SetNull)
|
||||
commissionRule CommonCityCommissionRule?
|
||||
stores Store[]
|
||||
orders Order[]
|
||||
warehouses CityWarehouse[]
|
||||
partnerAccounts PartnerAccount[] @relation("PartnerAccountCity")
|
||||
stores Store[]
|
||||
orders Order[]
|
||||
|
||||
@@index([partnerId])
|
||||
@@map("common_city")
|
||||
}
|
||||
|
||||
model CommonCityCommissionRule {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
cityId BigInt @unique @map("city_id") @db.UnsignedBigInt
|
||||
orderCommissionRate Decimal @default(0) @map("order_commission_rate") @db.Decimal(5, 4)
|
||||
redeemCommissionRate Decimal @default(0) @map("redeem_commission_rate") @db.Decimal(5, 4)
|
||||
partnerProfitRate Decimal @default(0.35) @map("partner_profit_rate") @db.Decimal(5, 4)
|
||||
storeSettlementRate Decimal @default(0.60) @map("store_settlement_rate") @db.Decimal(5, 4)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
model CityWarehouse {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
cityId BigInt @map("city_id") @db.UnsignedBigInt
|
||||
name String @db.VarChar(128)
|
||||
address String @db.VarChar(256)
|
||||
contactName String @map("contact_name") @db.VarChar(64)
|
||||
contactPhone String @map("contact_phone") @db.VarChar(20)
|
||||
managerType WarehouseManagerType @map("manager_type")
|
||||
partnerAccountId BigInt? @map("partner_account_id") @db.UnsignedBigInt
|
||||
status WarehouseStatus @default(ACTIVE)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
city CommonCity @relation(fields: [cityId], references: [id], onDelete: Cascade)
|
||||
city CommonCity @relation(fields: [cityId], references: [id], onDelete: Cascade)
|
||||
partnerAccount PartnerAccount? @relation("WarehouseManager", fields: [partnerAccountId], references: [id], onDelete: SetNull)
|
||||
managedBy PartnerAccount? @relation("ManagedWarehouse")
|
||||
|
||||
@@map("common_city_commission_rule")
|
||||
@@index([cityId])
|
||||
@@index([partnerAccountId])
|
||||
@@map("common_city_warehouse")
|
||||
}
|
||||
|
||||
// ─── PARTNER ──────────────────────────────────────────
|
||||
|
||||
model Partner {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
companyName String @map("company_name") @db.VarChar(128)
|
||||
address String @db.VarChar(256)
|
||||
contactPhone String @map("contact_phone") @db.VarChar(20)
|
||||
contractNo String? @map("contract_no") @db.VarChar(64)
|
||||
contractSignedAt DateTime? @map("contract_signed_at") @db.DateTime(3)
|
||||
contractExpireAt DateTime? @map("contract_expire_at") @db.DateTime(3)
|
||||
bankAccountName String? @map("bank_account_name") @db.VarChar(64)
|
||||
bankAccountNo String? @map("bank_account_no") @db.VarChar(32)
|
||||
bankBranch String? @map("bank_branch") @db.VarChar(128)
|
||||
weeklyStoreTarget Int? @default(20) @map("weekly_store_target")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
cities CommonCity[]
|
||||
accounts PartnerAccount[]
|
||||
stores Store[]
|
||||
bills PartnerBill[]
|
||||
|
||||
@@index([contactPhone])
|
||||
@@map("partner_partner")
|
||||
}
|
||||
|
||||
model PartnerAccount {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
partnerId BigInt @map("partner_id") @db.UnsignedBigInt
|
||||
phone String @unique @db.VarChar(20)
|
||||
name String @db.VarChar(64)
|
||||
wxOpenId String? @map("wx_open_id") @db.VarChar(64)
|
||||
wxUnionId String? @map("wx_union_id") @db.VarChar(64)
|
||||
isPrimary Int @default(0) @map("is_primary") @db.TinyInt
|
||||
parentAccountId BigInt? @map("parent_account_id") @db.UnsignedBigInt
|
||||
staffRole PartnerStaffRole? @map("staff_role")
|
||||
status AccountStatus @default(ACTIVE)
|
||||
lastLoginAt DateTime? @map("last_login_at") @db.DateTime(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
phone String @unique @db.VarChar(20)
|
||||
name String @db.VarChar(64)
|
||||
wxOpenId String? @map("wx_open_id") @db.VarChar(64)
|
||||
wxUnionId String? @map("wx_union_id") @db.VarChar(64)
|
||||
isPrimary Int @default(0) @map("is_primary") @db.TinyInt
|
||||
parentAccountId BigInt? @map("parent_account_id") @db.UnsignedBigInt
|
||||
staffRole PartnerStaffRole? @map("staff_role")
|
||||
permissions Json?
|
||||
status AccountStatus @default(ACTIVE)
|
||||
lastLoginAt DateTime? @map("last_login_at") @db.DateTime(3)
|
||||
cityId BigInt? @map("city_id") @db.UnsignedBigInt
|
||||
scopeType CityPartnerScopeType? @map("scope_type")
|
||||
districtCodes Json? @map("district_codes")
|
||||
orderCommissionRate Decimal? @default(0) @map("order_commission_rate") @db.Decimal(5, 4)
|
||||
redeemCommissionRate Decimal? @default(0.03) @map("redeem_commission_rate") @db.Decimal(5, 4)
|
||||
bindingStatus CityPartnerStatus? @default(ACTIVE) @map("binding_status")
|
||||
companyName String? @map("company_name") @db.VarChar(128)
|
||||
address String? @db.VarChar(256)
|
||||
contactPhone String? @map("contact_phone") @db.VarChar(20)
|
||||
contractNo String? @map("contract_no") @db.VarChar(64)
|
||||
contractSignedAt DateTime? @map("contract_signed_at") @db.DateTime(3)
|
||||
contractExpireAt DateTime? @map("contract_expire_at") @db.DateTime(3)
|
||||
bankAccountName String? @map("bank_account_name") @db.VarChar(64)
|
||||
bankAccountNo String? @map("bank_account_no") @db.VarChar(32)
|
||||
bankBranch String? @map("bank_branch") @db.VarChar(128)
|
||||
weeklyStoreTarget Int? @default(20) @map("weekly_store_target")
|
||||
managedWarehouseId BigInt? @unique @map("managed_warehouse_id") @db.UnsignedBigInt
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Restrict)
|
||||
parent PartnerAccount? @relation("PartnerAccountHierarchy", fields: [parentAccountId], references: [id], onDelete: SetNull)
|
||||
children PartnerAccount[] @relation("PartnerAccountHierarchy")
|
||||
city CommonCity? @relation("PartnerAccountCity", fields: [cityId], references: [id], onDelete: Restrict)
|
||||
managedWarehouse CityWarehouse? @relation("ManagedWarehouse", fields: [managedWarehouseId], references: [id], onDelete: SetNull)
|
||||
managedWarehouses CityWarehouse[] @relation("WarehouseManager")
|
||||
parent PartnerAccount? @relation("PartnerAccountHierarchy", fields: [parentAccountId], references: [id], onDelete: SetNull)
|
||||
children PartnerAccount[] @relation("PartnerAccountHierarchy")
|
||||
stores Store[]
|
||||
bills PartnerBill[]
|
||||
|
||||
@@index([partnerId])
|
||||
@@index([cityId, scopeType])
|
||||
@@index([cityId, isPrimary])
|
||||
@@index([parentAccountId])
|
||||
@@index([wxOpenId])
|
||||
@@index([contactPhone])
|
||||
@@map("partner_account")
|
||||
}
|
||||
|
||||
model PartnerBill {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
billNo String @unique @map("bill_no") @db.VarChar(32)
|
||||
partnerId BigInt @map("partner_id") @db.UnsignedBigInt
|
||||
periodStart DateTime @map("period_start") @db.DateTime(3)
|
||||
periodEnd DateTime @map("period_end") @db.DateTime(3)
|
||||
orderCommission Decimal @default(0) @map("order_commission") @db.Decimal(10, 2)
|
||||
redeemCommission Decimal @default(0) @map("redeem_commission") @db.Decimal(10, 2)
|
||||
totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
|
||||
status PartnerBillStatus @default(DRAFT)
|
||||
confirmedAt DateTime? @map("confirmed_at") @db.DateTime(3)
|
||||
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
billNo String @unique @map("bill_no") @db.VarChar(32)
|
||||
partnerAccountId BigInt @map("partner_account_id") @db.UnsignedBigInt
|
||||
periodStart DateTime @map("period_start") @db.DateTime(3)
|
||||
periodEnd DateTime @map("period_end") @db.DateTime(3)
|
||||
orderCommission Decimal @default(0) @map("order_commission") @db.Decimal(10, 2)
|
||||
redeemCommission Decimal @default(0) @map("redeem_commission") @db.Decimal(10, 2)
|
||||
totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
|
||||
status PartnerBillStatus @default(DRAFT)
|
||||
confirmedAt DateTime? @map("confirmed_at") @db.DateTime(3)
|
||||
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
|
||||
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Restrict)
|
||||
partnerAccount PartnerAccount @relation(fields: [partnerAccountId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@index([partnerId, status])
|
||||
@@index([partnerAccountId, status])
|
||||
@@map("partner_bill")
|
||||
}
|
||||
|
||||
@@ -626,7 +652,7 @@ model UserPromoAttribution {
|
||||
model Store {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
cityId BigInt @map("city_id") @db.UnsignedBigInt
|
||||
partnerId BigInt @map("partner_id") @db.UnsignedBigInt
|
||||
partnerAccountId BigInt @map("partner_account_id") @db.UnsignedBigInt
|
||||
categoryId BigInt? @map("category_id") @db.UnsignedBigInt
|
||||
name String @db.VarChar(128)
|
||||
phone String @db.VarChar(20)
|
||||
@@ -647,11 +673,12 @@ model Store {
|
||||
bankAccountName String? @map("bank_account_name") @db.VarChar(64)
|
||||
bankAccountNo String? @map("bank_account_no") @db.VarChar(32)
|
||||
bankBranch String? @map("bank_branch") @db.VarChar(128)
|
||||
settlementRate Decimal @default(0.60) @map("settlement_rate") @db.Decimal(5, 4)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
cityRef CommonCity @relation(fields: [cityId], references: [id], onDelete: Restrict)
|
||||
partner Partner @relation(fields: [partnerId], references: [id], onDelete: Restrict)
|
||||
partnerAccount PartnerAccount @relation(fields: [partnerAccountId], references: [id], onDelete: Restrict)
|
||||
category CommonStoreCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
||||
coverResource CommonResource? @relation("StoreCover", fields: [coverResourceId], references: [id], onDelete: SetNull)
|
||||
account StoreAccount?
|
||||
@@ -660,7 +687,7 @@ model Store {
|
||||
payouts StorePayout[]
|
||||
|
||||
@@index([cityId, status])
|
||||
@@index([partnerId])
|
||||
@@index([partnerAccountId])
|
||||
@@map("store_store")
|
||||
}
|
||||
|
||||
@@ -730,10 +757,12 @@ model Order {
|
||||
shippedAt DateTime? @map("shipped_at") @db.DateTime(3)
|
||||
completedAt DateTime? @map("completed_at") @db.DateTime(3)
|
||||
cancelledAt DateTime? @map("cancelled_at") @db.DateTime(3)
|
||||
payExpireAt DateTime? @map("pay_expire_at") @db.DateTime(3)
|
||||
remark String? @db.VarChar(512)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
payExpireAt DateTime? @map("pay_expire_at") @db.DateTime(3)
|
||||
partnerAccountIdAtPay BigInt? @map("partner_account_id_at_pay") @db.UnsignedBigInt
|
||||
orderCommissionRateAtPay Decimal? @map("order_commission_rate_at_pay") @db.Decimal(5, 4)
|
||||
remark String? @db.VarChar(512)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
|
||||
city CommonCity @relation(fields: [cityId], references: [id], onDelete: Restrict)
|
||||
@@ -915,8 +944,7 @@ model LogStoreAnalytics {
|
||||
|
||||
model LogPartnerAnalytics {
|
||||
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||
partnerAccountId BigInt? @map("partner_account_id") @db.UnsignedBigInt
|
||||
partnerId BigInt @map("partner_id") @db.UnsignedBigInt
|
||||
partnerAccountId BigInt @map("partner_account_id") @db.UnsignedBigInt
|
||||
eventName String @map("event_name") @db.VarChar(64)
|
||||
clientApp ClientApp? @map("client_app")
|
||||
refType String? @map("ref_type") @db.VarChar(32)
|
||||
@@ -924,7 +952,6 @@ model LogPartnerAnalytics {
|
||||
extraJson Json? @map("extra_json")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
|
||||
@@index([partnerId, createdAt])
|
||||
@@index([partnerAccountId, createdAt])
|
||||
@@index([eventName, createdAt])
|
||||
@@map("log_partner_analytics")
|
||||
|
||||
@@ -1,421 +1,873 @@
|
||||
import { PrismaClient, ResourceBizType, ResourceMediaType, ResourceOwnerType } from '@prisma/client';
|
||||
|
||||
import { hashPassword } from '../src/common/crypto/password.util';
|
||||
import { DEFAULT_PRODUCT_DETAIL_TEMPLATES } from './seeds/product-detail-templates.default';
|
||||
|
||||
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
||||
|
||||
async function createMockResource(
|
||||
|
||||
ownerType: ResourceOwnerType,
|
||||
|
||||
ownerId: bigint,
|
||||
|
||||
bizType: ResourceBizType,
|
||||
|
||||
url: string,
|
||||
|
||||
) {
|
||||
|
||||
return prisma.commonResource.create({
|
||||
|
||||
data: {
|
||||
|
||||
ownerType,
|
||||
|
||||
ownerId,
|
||||
|
||||
bizType,
|
||||
|
||||
mediaType: ResourceMediaType.IMAGE,
|
||||
|
||||
ossBucket: 'mock-dukang',
|
||||
|
||||
ossKey: `mock/${ownerType.toLowerCase()}/${ownerId}/${bizType.toLowerCase()}`,
|
||||
|
||||
url,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function main() {
|
||||
|
||||
console.log('Seeding v3.1 data...');
|
||||
|
||||
|
||||
|
||||
await prisma.logUserAnalytics.deleteMany();
|
||||
|
||||
await prisma.logPartnerAnalytics.deleteMany();
|
||||
|
||||
await prisma.logThirdParty.deleteMany();
|
||||
|
||||
await prisma.storePayout.deleteMany();
|
||||
|
||||
await prisma.storeRating.deleteMany();
|
||||
|
||||
await prisma.redeemRecord.deleteMany();
|
||||
|
||||
await prisma.benefitCoupon.deleteMany();
|
||||
|
||||
await prisma.orderDelivery.deleteMany();
|
||||
|
||||
await prisma.order.deleteMany();
|
||||
|
||||
await prisma.commonEvent.deleteMany();
|
||||
|
||||
await prisma.commonTicket.deleteMany();
|
||||
|
||||
await prisma.userPromoAttribution.deleteMany();
|
||||
|
||||
await prisma.userCityPreference.deleteMany();
|
||||
|
||||
await prisma.userAddress.deleteMany();
|
||||
|
||||
await prisma.user.deleteMany();
|
||||
|
||||
await prisma.storeAccount.deleteMany();
|
||||
|
||||
await prisma.store.deleteMany();
|
||||
|
||||
await prisma.partnerBill.deleteMany();
|
||||
|
||||
await prisma.cityWarehouse.deleteMany();
|
||||
|
||||
await prisma.partnerAccount.deleteMany();
|
||||
await prisma.commonCityCommissionRule.deleteMany();
|
||||
|
||||
await prisma.commonCity.deleteMany();
|
||||
await prisma.partner.deleteMany();
|
||||
|
||||
await prisma.commonProductItem.deleteMany();
|
||||
|
||||
await prisma.commonProductDetailTemplate.deleteMany();
|
||||
|
||||
await prisma.commonStoreCategory.deleteMany();
|
||||
|
||||
await prisma.commonPromoCode.deleteMany();
|
||||
|
||||
await prisma.commonResource.deleteMany();
|
||||
|
||||
await prisma.hqAccount.deleteMany();
|
||||
|
||||
const partner = await prisma.partner.create({
|
||||
data: {
|
||||
companyName: '郑州城市合伙人',
|
||||
address: '河南省郑州市金水区',
|
||||
contactPhone: '13700000001',
|
||||
bankAccountName: '郑州合伙人公司',
|
||||
bankAccountNo: '6222021234567890',
|
||||
bankBranch: '工商银行郑州分行',
|
||||
weeklyStoreTarget: 20,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const city = await prisma.commonCity.create({
|
||||
|
||||
data: {
|
||||
|
||||
code: '410100',
|
||||
|
||||
name: '郑州市',
|
||||
|
||||
province: '河南省',
|
||||
|
||||
status: 'ACTIVE',
|
||||
partnerId: partner.id,
|
||||
|
||||
localMinQty: 2,
|
||||
|
||||
crossMinQty: 6,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
await prisma.commonCityCommissionRule.create({
|
||||
|
||||
|
||||
const primaryAccount = await prisma.partnerAccount.create({
|
||||
|
||||
data: {
|
||||
|
||||
phone: '13700000001',
|
||||
|
||||
name: '郑州合伙人主账号',
|
||||
|
||||
isPrimary: 1,
|
||||
|
||||
staffRole: 'PARTNER',
|
||||
|
||||
status: 'ACTIVE',
|
||||
|
||||
cityId: city.id,
|
||||
|
||||
scopeType: 'CITY_WIDE',
|
||||
|
||||
orderCommissionRate: 0,
|
||||
|
||||
redeemCommissionRate: 0.03,
|
||||
partnerProfitRate: 0.35,
|
||||
storeSettlementRate: 0.6,
|
||||
|
||||
bindingStatus: 'ACTIVE',
|
||||
|
||||
companyName: '郑州城市合伙人',
|
||||
|
||||
address: '河南省郑州市金水区',
|
||||
|
||||
contactPhone: '13700000001',
|
||||
|
||||
bankAccountName: '郑州合伙人公司',
|
||||
|
||||
bankAccountNo: '6222021234567890',
|
||||
|
||||
bankBranch: '工商银行郑州分行',
|
||||
|
||||
weeklyStoreTarget: 20,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const warehouse = await prisma.cityWarehouse.create({
|
||||
|
||||
data: {
|
||||
|
||||
cityId: city.id,
|
||||
|
||||
name: '郑州中央仓',
|
||||
|
||||
address: '河南省郑州市金水区物流园1号',
|
||||
|
||||
contactName: '仓管张',
|
||||
|
||||
contactPhone: '13700000009',
|
||||
|
||||
managerType: 'PARTNER',
|
||||
|
||||
partnerAccountId: primaryAccount.id,
|
||||
|
||||
status: 'ACTIVE',
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
await prisma.partnerAccount.update({
|
||||
|
||||
where: { id: primaryAccount.id },
|
||||
|
||||
data: { managedWarehouseId: warehouse.id },
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
await prisma.partnerAccount.create({
|
||||
|
||||
data: {
|
||||
|
||||
phone: '13700000002',
|
||||
|
||||
name: '拓店员小李',
|
||||
|
||||
isPrimary: 0,
|
||||
|
||||
parentAccountId: primaryAccount.id,
|
||||
|
||||
staffRole: 'INTERNAL',
|
||||
|
||||
permissions: ['store:create', 'store:view'],
|
||||
|
||||
status: 'DISABLED',
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const categories = await Promise.all([
|
||||
|
||||
prisma.commonStoreCategory.create({ data: { code: 'HOTPOT', name: '火锅', sort: 1 } }),
|
||||
|
||||
prisma.commonStoreCategory.create({ data: { code: 'LOCAL', name: '地方菜', sort: 2 } }),
|
||||
|
||||
]);
|
||||
|
||||
|
||||
|
||||
for (const tpl of DEFAULT_PRODUCT_DETAIL_TEMPLATES) {
|
||||
|
||||
await prisma.commonProductDetailTemplate.create({
|
||||
|
||||
data: {
|
||||
|
||||
code: tpl.code,
|
||||
|
||||
name: tpl.name,
|
||||
|
||||
description: tpl.description,
|
||||
|
||||
aromaType: tpl.aromaType as 'QINGXIANG' | 'JIANGXIANG' | 'NONGXIANG' | null,
|
||||
|
||||
storyTitle: tpl.storyTitle,
|
||||
|
||||
storyText: tpl.storyText,
|
||||
|
||||
features: tpl.features,
|
||||
|
||||
detailImageUrls: [],
|
||||
|
||||
suggestedDetailImageCount: tpl.suggestedDetailImageCount,
|
||||
|
||||
sortOrder: tpl.sortOrder,
|
||||
|
||||
status: 'ACTIVE',
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const productDefs = [
|
||||
|
||||
{ skuCode: 'JZ-10', name: '酒祖杜康(国标特级10)', subtitle: '清香型 53度', price: 128, sortOrder: 1, img: 'https://picsum.photos/seed/jiuzu10/400/400' },
|
||||
|
||||
{ skuCode: 'JZ-15', name: '酒祖杜康(国标特级15)', subtitle: '清香型 53度', price: 168, sortOrder: 2, img: 'https://picsum.photos/seed/jiuzu15/400/400' },
|
||||
|
||||
{ skuCode: 'JZ-20', name: '酒祖杜康(国标特级20)', subtitle: '清香型 53度', price: 298, sortOrder: 3, img: 'https://picsum.photos/seed/jiuzu20/400/400' },
|
||||
|
||||
{ skuCode: 'JZ-30', name: '酒祖杜康(国标特级30)', subtitle: '清香型 53度', price: 498, sortOrder: 4, img: 'https://picsum.photos/seed/jiuzu30/400/400' },
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
const products = [];
|
||||
|
||||
for (const [i, def] of productDefs.entries()) {
|
||||
|
||||
const product = await prisma.commonProductItem.create({
|
||||
|
||||
data: {
|
||||
|
||||
skuCode: def.skuCode,
|
||||
|
||||
barcode69: `69000000000${i + 1}`,
|
||||
|
||||
name: def.name,
|
||||
|
||||
subtitle: def.subtitle,
|
||||
|
||||
aromaType: 'QINGXIANG',
|
||||
|
||||
spec: '500ml | 53度',
|
||||
|
||||
price: def.price,
|
||||
|
||||
benefitAmount: def.price,
|
||||
|
||||
status: 'ON_SALE',
|
||||
|
||||
sortOrder: def.sortOrder,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const cover = await createMockResource(ResourceOwnerType.PRODUCT, product.id, ResourceBizType.COVER, def.img);
|
||||
|
||||
await prisma.commonProductItem.update({
|
||||
|
||||
where: { id: product.id },
|
||||
|
||||
data: { coverResourceId: cover.id },
|
||||
|
||||
});
|
||||
|
||||
products.push(product);
|
||||
|
||||
}
|
||||
|
||||
await prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: partner.id,
|
||||
phone: '13700000001',
|
||||
name: '郑州合伙人主账号',
|
||||
isPrimary: 1,
|
||||
staffRole: 'PARTNER',
|
||||
},
|
||||
});
|
||||
|
||||
const primaryAccount = await prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { phone: '13700000001' },
|
||||
});
|
||||
|
||||
await prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: partner.id,
|
||||
phone: '13700000002',
|
||||
name: '拓店员小李',
|
||||
isPrimary: 0,
|
||||
parentAccountId: primaryAccount.id,
|
||||
staffRole: 'INTERNAL',
|
||||
status: 'DISABLED',
|
||||
},
|
||||
});
|
||||
|
||||
const storeDefs = [
|
||||
|
||||
{
|
||||
|
||||
name: '郑州老城店',
|
||||
|
||||
phone: '13910000001',
|
||||
|
||||
district: '金水区',
|
||||
|
||||
address: '花园路100号',
|
||||
|
||||
intro: '正宗河南菜,欢迎核销好客权益',
|
||||
|
||||
img: 'https://picsum.photos/seed/store1/400/300',
|
||||
|
||||
categoryId: categories[0].id,
|
||||
|
||||
settlementRate: 0.6,
|
||||
|
||||
withAccount: true,
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
name: '郑州美食城店',
|
||||
|
||||
phone: '13910000002',
|
||||
|
||||
district: '二七区',
|
||||
|
||||
address: '大学路200号',
|
||||
|
||||
intro: '地方特色餐饮',
|
||||
|
||||
img: 'https://picsum.photos/seed/store2/400/300',
|
||||
|
||||
categoryId: categories[1].id,
|
||||
|
||||
settlementRate: 0.65,
|
||||
|
||||
withAccount: false,
|
||||
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
const createdStores: { id: bigint; name: string }[] = [];
|
||||
|
||||
for (const def of storeDefs) {
|
||||
|
||||
const store = await prisma.store.create({
|
||||
|
||||
data: {
|
||||
|
||||
cityId: city.id,
|
||||
partnerId: partner.id,
|
||||
|
||||
partnerAccountId: primaryAccount.id,
|
||||
|
||||
categoryId: def.categoryId,
|
||||
|
||||
name: def.name,
|
||||
|
||||
phone: def.phone,
|
||||
|
||||
province: '河南省',
|
||||
|
||||
cityName: '郑州市',
|
||||
|
||||
district: def.district,
|
||||
|
||||
address: def.address,
|
||||
|
||||
intro: def.intro,
|
||||
|
||||
settlementRate: def.settlementRate,
|
||||
|
||||
status: 'OPEN',
|
||||
|
||||
openTime: '10:00',
|
||||
|
||||
closeTime: '22:00',
|
||||
|
||||
bankAccountName: def.name,
|
||||
|
||||
bankAccountNo: '6222029876543210',
|
||||
|
||||
bankBranch: '建设银行郑州分行',
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const cover = await createMockResource(ResourceOwnerType.STORE, store.id, ResourceBizType.COVER, def.img);
|
||||
|
||||
await prisma.store.update({ where: { id: store.id }, data: { coverResourceId: cover.id } });
|
||||
|
||||
if (def.withAccount) {
|
||||
|
||||
await prisma.storeAccount.create({
|
||||
|
||||
data: { storeId: store.id, phone: def.phone, name: def.name },
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
createdStores.push({ id: store.id, name: def.name });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const weekStart = (() => {
|
||||
|
||||
const d = new Date();
|
||||
|
||||
d.setHours(12, 0, 0, 0);
|
||||
|
||||
const day = d.getDay();
|
||||
|
||||
const diff = day === 0 ? 6 : day - 1;
|
||||
|
||||
d.setDate(d.getDate() - diff);
|
||||
|
||||
d.setHours(10, 0, 0, 0);
|
||||
|
||||
return d;
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
const newWeekStore = await prisma.store.create({
|
||||
|
||||
data: {
|
||||
|
||||
cityId: city.id,
|
||||
partnerId: partner.id,
|
||||
|
||||
partnerAccountId: primaryAccount.id,
|
||||
|
||||
categoryId: categories[0].id,
|
||||
|
||||
name: '本周新签体验店',
|
||||
|
||||
phone: '13910000003',
|
||||
|
||||
province: '河南省',
|
||||
|
||||
cityName: '郑州市',
|
||||
|
||||
district: '中原区',
|
||||
|
||||
address: '建设路88号',
|
||||
|
||||
intro: '本周新签约门店',
|
||||
|
||||
settlementRate: 0.6,
|
||||
|
||||
status: 'OPEN',
|
||||
|
||||
openTime: '10:00',
|
||||
|
||||
closeTime: '22:00',
|
||||
|
||||
bankAccountName: '本周新签体验店',
|
||||
|
||||
bankAccountNo: '6222029876543211',
|
||||
|
||||
bankBranch: '农业银行郑州分行',
|
||||
|
||||
createdAt: new Date(weekStart.getTime() + 2 * 24 * 60 * 60 * 1000),
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
createdStores.push({ id: newWeekStore.id, name: newWeekStore.name });
|
||||
|
||||
|
||||
|
||||
await prisma.user.create({
|
||||
|
||||
data: {
|
||||
|
||||
userNo: 'DK88293401',
|
||||
|
||||
phone: '13800000001',
|
||||
|
||||
phoneVerifiedAt: new Date(),
|
||||
|
||||
nickname: '测试用户',
|
||||
|
||||
cityPreference: {
|
||||
|
||||
create: {
|
||||
|
||||
selectedCityCode: '410100',
|
||||
|
||||
selectedDistrict: '郑州市',
|
||||
|
||||
locateCityCode: '410100',
|
||||
|
||||
locateDistrict: '金水区',
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
await prisma.hqAccount.create({
|
||||
|
||||
data: {
|
||||
|
||||
phone: '13600000001',
|
||||
|
||||
loginName: 'admin',
|
||||
|
||||
passwordHash: hashPassword('dukang@123!'),
|
||||
|
||||
name: '总部管理员',
|
||||
|
||||
adminRole: 'SUPER_ADMIN',
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
await prisma.commonPromoCode.create({
|
||||
|
||||
data: {
|
||||
|
||||
code: 'DKHQ001',
|
||||
|
||||
name: '总部品鉴会',
|
||||
|
||||
status: 'ACTIVE',
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
await prisma.commonPromoCode.create({
|
||||
|
||||
data: {
|
||||
|
||||
code: 'DKDEMO1',
|
||||
|
||||
name: '郑州品鉴会演示',
|
||||
|
||||
status: 'ACTIVE',
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { phone: '13800000001' } });
|
||||
|
||||
const product = products[0];
|
||||
|
||||
|
||||
|
||||
const orderDefs = [
|
||||
|
||||
{ dayOffset: 0, payAmount: 1198, quantity: 2 },
|
||||
|
||||
{ dayOffset: 1, payAmount: 599, quantity: 1 },
|
||||
|
||||
{ dayOffset: 2, payAmount: 1760, quantity: 2 },
|
||||
|
||||
{ dayOffset: 4, payAmount: 1299, quantity: 1 },
|
||||
|
||||
{ dayOffset: 5, payAmount: 880, quantity: 1 },
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
const coupons: { id: bigint; balance: number }[] = [];
|
||||
|
||||
for (const [index, def] of orderDefs.entries()) {
|
||||
|
||||
const paidAt = new Date(weekStart.getTime() + def.dayOffset * 24 * 60 * 60 * 1000 + 14 * 60 * 60 * 1000);
|
||||
|
||||
const listAmount = def.payAmount;
|
||||
|
||||
const order = await prisma.order.create({
|
||||
|
||||
data: {
|
||||
|
||||
orderNo: `WK${Date.now()}${index}`,
|
||||
|
||||
userId: user.id,
|
||||
|
||||
cityId: city.id,
|
||||
|
||||
status: 'COMPLETED',
|
||||
|
||||
payStatus: 'PAID',
|
||||
|
||||
deliveryType: 'LOCAL',
|
||||
|
||||
productId: product.id,
|
||||
|
||||
barcode69: product.barcode69,
|
||||
|
||||
productName: product.name,
|
||||
|
||||
productSpec: product.spec,
|
||||
|
||||
quantity: def.quantity,
|
||||
|
||||
listUnitPrice: product.price,
|
||||
|
||||
listAmount,
|
||||
|
||||
productAmount: listAmount,
|
||||
|
||||
payAmount: listAmount,
|
||||
|
||||
benefitAmount: listAmount,
|
||||
|
||||
receiverName: '测试用户',
|
||||
receiverPhone: user.phone,
|
||||
|
||||
receiverPhone: user.phone!,
|
||||
|
||||
receiverAddress: '郑州市金水区测试路1号',
|
||||
|
||||
receiverProvince: '河南省',
|
||||
|
||||
receiverCity: '郑州市',
|
||||
|
||||
receiverDistrict: '金水区',
|
||||
|
||||
paidAt,
|
||||
|
||||
completedAt: paidAt,
|
||||
|
||||
partnerAccountIdAtPay: primaryAccount.id,
|
||||
|
||||
orderCommissionRateAtPay: 0,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const coupon = await prisma.benefitCoupon.create({
|
||||
|
||||
data: {
|
||||
|
||||
couponNo: `CPN${Date.now()}${index}`,
|
||||
|
||||
userId: user.id,
|
||||
|
||||
orderId: order.id,
|
||||
|
||||
totalAmount: listAmount,
|
||||
|
||||
balance: listAmount,
|
||||
|
||||
sourceProduct: product.name,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
coupons.push({ id: coupon.id, balance: listAmount });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const redeemDefs = [
|
||||
{ storeIndex: 0, amount: 42800, dayOffset: 1 },
|
||||
{ storeIndex: 1, amount: 38400, dayOffset: 2 },
|
||||
{ storeIndex: 2, amount: 31200, dayOffset: 3 },
|
||||
|
||||
{ storeIndex: 0, amount: 42800, dayOffset: 1, settlementRate: 0.6 },
|
||||
|
||||
{ storeIndex: 1, amount: 38400, dayOffset: 2, settlementRate: 0.65 },
|
||||
|
||||
{ storeIndex: 2, amount: 31200, dayOffset: 3, settlementRate: 0.6 },
|
||||
|
||||
];
|
||||
|
||||
for (const [index, def] of redeemDefs.entries()) {
|
||||
|
||||
const coupon = coupons[index];
|
||||
|
||||
const store = createdStores[def.storeIndex];
|
||||
|
||||
const createdAt = new Date(weekStart.getTime() + def.dayOffset * 24 * 60 * 60 * 1000 + 16 * 60 * 60 * 1000);
|
||||
|
||||
const settleAmount = Math.round(def.amount * def.settlementRate * 100) / 100;
|
||||
|
||||
await prisma.redeemRecord.create({
|
||||
|
||||
data: {
|
||||
|
||||
redeemNo: `RD${Date.now()}${index}`,
|
||||
|
||||
userId: user.id,
|
||||
|
||||
couponId: coupon.id,
|
||||
|
||||
storeId: store.id,
|
||||
|
||||
amount: def.amount,
|
||||
settleAmount: Math.round(def.amount * 0.6 * 100) / 100,
|
||||
|
||||
settleAmount,
|
||||
|
||||
createdAt,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
await prisma.benefitCoupon.update({
|
||||
|
||||
where: { id: coupon.id },
|
||||
|
||||
data: {
|
||||
|
||||
usedAmount: def.amount,
|
||||
|
||||
balance: Math.max(0, coupon.balance - def.amount),
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
|
||||
await prisma.partnerBill.create({
|
||||
|
||||
data: {
|
||||
|
||||
billNo: `PB${Date.now()}`,
|
||||
partnerId: partner.id,
|
||||
|
||||
partnerAccountId: primaryAccount.id,
|
||||
|
||||
periodStart,
|
||||
|
||||
periodEnd,
|
||||
|
||||
orderCommission: 1200,
|
||||
|
||||
redeemCommission: 800,
|
||||
|
||||
totalAmount: 2000,
|
||||
|
||||
status: 'CONFIRMED',
|
||||
|
||||
confirmedAt: now,
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
console.log('Seed complete:', {
|
||||
|
||||
city: city.name,
|
||||
|
||||
warehouse: warehouse.name,
|
||||
|
||||
primaryAccount: primaryAccount.phone,
|
||||
|
||||
detailTemplates: DEFAULT_PRODUCT_DETAIL_TEMPLATES.length,
|
||||
|
||||
products: products.length,
|
||||
stores: storeDefs.length,
|
||||
|
||||
stores: createdStores.length,
|
||||
|
||||
testPhones: {
|
||||
|
||||
user: '13800000001',
|
||||
|
||||
partner: '13700000001',
|
||||
|
||||
partnerStaff: '13700000002',
|
||||
|
||||
hq: '13600000001',
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
main()
|
||||
|
||||
.catch((e) => {
|
||||
|
||||
console.error(e);
|
||||
|
||||
process.exit(1);
|
||||
|
||||
})
|
||||
|
||||
.finally(async () => {
|
||||
|
||||
await prisma.$disconnect();
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SettlementModule } from './modules/settlement/settlement.module';
|
||||
import { AnalyticsModule } from './modules/analytics/analytics.module';
|
||||
import { JobsModule } from './jobs/jobs.module';
|
||||
import { OpsModule } from './modules/ops/ops.module';
|
||||
import { CityScopeModule } from './modules/city-scope/city-scope.module';
|
||||
import { CommonModule } from './modules/common/common.module';
|
||||
import { HqOperationModule } from './common/hq-operation/hq-operation.module';
|
||||
import { CallbacksModule } from './callbacks/callbacks.module';
|
||||
@@ -41,6 +42,7 @@ import { CallbacksModule } from './callbacks/callbacks.module';
|
||||
AnalyticsModule,
|
||||
JobsModule,
|
||||
OpsModule,
|
||||
CityScopeModule,
|
||||
CommonModule,
|
||||
HqOperationModule,
|
||||
CallbacksModule,
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
export const HqOperationAction = {
|
||||
CITY_CREATE: 'CITY_CREATE',
|
||||
CITY_UPDATE: 'CITY_UPDATE',
|
||||
CITY_DELETE: 'CITY_DELETE',
|
||||
CITY_PARTNER_BIND: 'CITY_PARTNER_BIND',
|
||||
CITY_PARTNER_UPDATE: 'CITY_PARTNER_UPDATE',
|
||||
CITY_PARTNER_UNBIND: 'CITY_PARTNER_UNBIND',
|
||||
WAREHOUSE_CREATE: 'WAREHOUSE_CREATE',
|
||||
WAREHOUSE_UPDATE: 'WAREHOUSE_UPDATE',
|
||||
WAREHOUSE_DELETE: 'WAREHOUSE_DELETE',
|
||||
PARTNER_CREATE: 'PARTNER_CREATE',
|
||||
PARTNER_UPDATE: 'PARTNER_UPDATE',
|
||||
PARTNER_ACCOUNT_CREATE: 'PARTNER_ACCOUNT_CREATE',
|
||||
@@ -47,6 +54,13 @@ export type HqOperationActionCode = (typeof HqOperationAction)[keyof typeof HqOp
|
||||
export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
|
||||
[HqOperationAction.CITY_CREATE]: '新增开城城市',
|
||||
[HqOperationAction.CITY_UPDATE]: '编辑开城城市',
|
||||
[HqOperationAction.CITY_DELETE]: '删除开城城市',
|
||||
[HqOperationAction.CITY_PARTNER_BIND]: '绑定城市合伙人',
|
||||
[HqOperationAction.CITY_PARTNER_UPDATE]: '编辑城市合伙人绑定',
|
||||
[HqOperationAction.CITY_PARTNER_UNBIND]: '解绑城市合伙人',
|
||||
[HqOperationAction.WAREHOUSE_CREATE]: '新增城市仓库',
|
||||
[HqOperationAction.WAREHOUSE_UPDATE]: '编辑城市仓库',
|
||||
[HqOperationAction.WAREHOUSE_DELETE]: '删除城市仓库',
|
||||
[HqOperationAction.PARTNER_CREATE]: '新增城市合伙人',
|
||||
[HqOperationAction.PARTNER_UPDATE]: '编辑城市合伙人',
|
||||
[HqOperationAction.PARTNER_ACCOUNT_CREATE]: '新增合伙人账户',
|
||||
|
||||
@@ -17,8 +17,8 @@ export type TrackStoreEventInput = TrackEventInput & {
|
||||
};
|
||||
|
||||
export type TrackPartnerEventInput = TrackEventInput & {
|
||||
partnerAccountId?: bigint;
|
||||
partnerId: bigint;
|
||||
/** 主账号 ID,用于合伙人维度聚合 */
|
||||
partnerAccountId: bigint;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -64,21 +64,21 @@ export class AnalyticsService {
|
||||
}
|
||||
|
||||
async trackPartnerOne(
|
||||
partnerAccountId: bigint | undefined,
|
||||
actorAccountId: bigint | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackPartnerEventInput,
|
||||
) {
|
||||
await this.prisma.logPartnerAnalytics.create({
|
||||
data: this.toPartnerRow(partnerAccountId, clientApp, event),
|
||||
data: this.toPartnerRow(actorAccountId, clientApp, event),
|
||||
});
|
||||
}
|
||||
|
||||
trackPartnerOneSafe(
|
||||
partnerAccountId: bigint | undefined,
|
||||
actorAccountId: bigint | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackPartnerEventInput,
|
||||
) {
|
||||
void this.trackPartnerOne(partnerAccountId, clientApp, event).catch(() => {});
|
||||
void this.trackPartnerOne(actorAccountId, clientApp, event).catch(() => {});
|
||||
}
|
||||
|
||||
private toRow(userId: bigint, clientApp: ClientApp | string, event: TrackEventInput) {
|
||||
@@ -110,17 +110,16 @@ export class AnalyticsService {
|
||||
}
|
||||
|
||||
private toPartnerRow(
|
||||
partnerAccountId: bigint | undefined,
|
||||
actorAccountId: bigint | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
event: TrackPartnerEventInput,
|
||||
) {
|
||||
return {
|
||||
partnerAccountId,
|
||||
partnerId: event.partnerId,
|
||||
partnerAccountId: event.partnerAccountId,
|
||||
eventName: event.eventName,
|
||||
clientApp: clientApp as ClientApp,
|
||||
refType: event.refType,
|
||||
refId: event.refId,
|
||||
refId: event.refId ?? actorAccountId,
|
||||
extraJson: event.extraJson as never,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,10 +10,28 @@ export class CatalogService {
|
||||
async listCities() {
|
||||
const cities = await this.prisma.commonCity.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
include: { partner: { select: { companyName: true } } },
|
||||
include: {
|
||||
partnerAccounts: {
|
||||
where: { isPrimary: 1, bindingStatus: 'ACTIVE' },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
select: { id: true, companyName: true, scopeType: true },
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
return serializeBigInt(cities);
|
||||
return serializeBigInt(
|
||||
cities.map((city) => ({
|
||||
...city,
|
||||
partnerBindingCount: city.partnerAccounts.length,
|
||||
partnerBindings: city.partnerAccounts.map((bp) => ({
|
||||
partnerAccountId: bp.id.toString(),
|
||||
partnerId: bp.id.toString(),
|
||||
companyName: bp.companyName,
|
||||
scopeType: bp.scopeType,
|
||||
})),
|
||||
partnerAccounts: undefined,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
async listProducts(aromaType?: string, cityCode?: string) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PartnerCityService } from './partner-city.service';
|
||||
import { CityWarehouseService } from './city-warehouse.service';
|
||||
|
||||
@Module({
|
||||
providers: [PartnerCityService, CityWarehouseService],
|
||||
exports: [PartnerCityService, CityWarehouseService],
|
||||
})
|
||||
export class CityScopeModule {}
|
||||
@@ -0,0 +1,207 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma, WarehouseManagerType, WarehouseStatus } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerCityService } from './partner-city.service';
|
||||
import type { AdminCityWarehousesQueryDto } from '../ops/dto/admin-query.dto';
|
||||
|
||||
export type CreateCityWarehouseInput = {
|
||||
name: string;
|
||||
address: string;
|
||||
contactName: string;
|
||||
contactPhone: string;
|
||||
managerType: WarehouseManagerType;
|
||||
partnerAccountId?: bigint;
|
||||
status?: WarehouseStatus;
|
||||
};
|
||||
|
||||
export type UpdateCityWarehouseInput = Partial<CreateCityWarehouseInput>;
|
||||
|
||||
@Injectable()
|
||||
export class CityWarehouseService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async listByCity(cityId: bigint) {
|
||||
const rows = await this.prisma.cityWarehouse.findMany({
|
||||
where: { cityId },
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return rows.map((row) => this.toDto(row));
|
||||
}
|
||||
|
||||
async listAll(query: AdminCityWarehousesQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.CityWarehouseWhereInput = {};
|
||||
if (query.cityId) where.cityId = BigInt(query.cityId);
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.managerType) where.managerType = query.managerType as Prisma.EnumWarehouseManagerTypeFilter['equals'];
|
||||
if (query.status) where.status = query.status as Prisma.EnumWarehouseStatusFilter['equals'];
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.cityWarehouse.findMany({
|
||||
where,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
city: { select: { id: true, name: true, code: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
this.prisma.cityWarehouse.count({ where }),
|
||||
]);
|
||||
|
||||
return serializeBigInt({
|
||||
items: rows.map((row) => ({
|
||||
...this.toDto(row),
|
||||
cityName: row.city.name,
|
||||
cityCode: row.city.code,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async create(cityId: bigint, input: CreateCityWarehouseInput) {
|
||||
await this.assertCityExists(cityId);
|
||||
await this.validateManager(input.managerType, input.partnerAccountId, cityId);
|
||||
|
||||
const row = await this.prisma.cityWarehouse.create({
|
||||
data: {
|
||||
cityId,
|
||||
name: input.name.trim(),
|
||||
address: input.address.trim(),
|
||||
contactName: input.contactName.trim(),
|
||||
contactPhone: input.contactPhone.trim(),
|
||||
managerType: input.managerType,
|
||||
partnerAccountId: input.managerType === 'PARTNER' ? input.partnerAccountId : null,
|
||||
status: input.status ?? 'ACTIVE',
|
||||
},
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
},
|
||||
});
|
||||
await this.syncManagedWarehouse(row.id, row.managerType as WarehouseManagerType, row.partnerAccountId);
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async update(id: bigint, input: UpdateCityWarehouseInput) {
|
||||
const current = await this.prisma.cityWarehouse.findUnique({ where: { id } });
|
||||
if (!current) throw new NotFoundException('仓库不存在');
|
||||
|
||||
const managerType = input.managerType ?? (current.managerType as WarehouseManagerType);
|
||||
const partnerAccountId =
|
||||
managerType === 'PARTNER'
|
||||
? input.partnerAccountId ?? current.partnerAccountId ?? undefined
|
||||
: null;
|
||||
|
||||
await this.validateManager(managerType, partnerAccountId ?? undefined, current.cityId);
|
||||
|
||||
const row = await this.prisma.cityWarehouse.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(input.name !== undefined ? { name: input.name.trim() } : {}),
|
||||
...(input.address !== undefined ? { address: input.address.trim() } : {}),
|
||||
...(input.contactName !== undefined ? { contactName: input.contactName.trim() } : {}),
|
||||
...(input.contactPhone !== undefined ? { contactPhone: input.contactPhone.trim() } : {}),
|
||||
...(input.managerType !== undefined ? { managerType: input.managerType } : {}),
|
||||
...(input.managerType !== undefined || input.partnerAccountId !== undefined
|
||||
? { partnerAccountId: managerType === 'PARTNER' ? partnerAccountId : null }
|
||||
: {}),
|
||||
...(input.status !== undefined ? { status: input.status } : {}),
|
||||
},
|
||||
include: {
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
},
|
||||
});
|
||||
await this.syncManagedWarehouse(row.id, row.managerType as WarehouseManagerType, row.partnerAccountId);
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
async remove(id: bigint) {
|
||||
const current = await this.prisma.cityWarehouse.findUnique({ where: { id } });
|
||||
if (!current) throw new NotFoundException('仓库不存在');
|
||||
await this.prisma.partnerAccount.updateMany({
|
||||
where: { managedWarehouseId: id },
|
||||
data: { managedWarehouseId: null },
|
||||
});
|
||||
await this.prisma.cityWarehouse.delete({ where: { id } });
|
||||
return { ok: true, id: id.toString() };
|
||||
}
|
||||
|
||||
private async syncManagedWarehouse(
|
||||
warehouseId: bigint,
|
||||
managerType: WarehouseManagerType,
|
||||
partnerAccountId: bigint | null,
|
||||
) {
|
||||
await this.prisma.partnerAccount.updateMany({
|
||||
where: { managedWarehouseId: warehouseId },
|
||||
data: { managedWarehouseId: null },
|
||||
});
|
||||
|
||||
if (managerType === 'PARTNER' && partnerAccountId) {
|
||||
await this.prisma.partnerAccount.updateMany({
|
||||
where: { id: partnerAccountId, managedWarehouseId: { not: warehouseId } },
|
||||
data: { managedWarehouseId: null },
|
||||
});
|
||||
await this.prisma.partnerAccount.update({
|
||||
where: { id: partnerAccountId },
|
||||
data: { managedWarehouseId: warehouseId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private validateManager(
|
||||
managerType: WarehouseManagerType,
|
||||
partnerAccountId: bigint | undefined,
|
||||
cityId: bigint,
|
||||
) {
|
||||
if (managerType === 'PARTNER') {
|
||||
if (!partnerAccountId) throw new BadRequestException('合伙人管仓须指定合伙人');
|
||||
return this.partnerCityService.assertPartnerAccountBoundToCity(partnerAccountId, cityId);
|
||||
}
|
||||
}
|
||||
|
||||
private async assertCityExists(cityId: bigint) {
|
||||
const city = await this.prisma.commonCity.findUnique({ where: { id: cityId } });
|
||||
if (!city) throw new NotFoundException('开城城市不存在');
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: bigint;
|
||||
cityId: bigint;
|
||||
name: string;
|
||||
address: string;
|
||||
contactName: string;
|
||||
contactPhone: string;
|
||||
managerType: string;
|
||||
partnerAccountId: bigint | null;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
partnerAccount?: { id: bigint; companyName: string | null } | null;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id.toString(),
|
||||
cityId: row.cityId.toString(),
|
||||
name: row.name,
|
||||
address: row.address,
|
||||
contactName: row.contactName,
|
||||
contactPhone: row.contactPhone,
|
||||
managerType: row.managerType,
|
||||
partnerAccountId: row.partnerAccountId?.toString() ?? null,
|
||||
partnerCompanyName: row.partnerAccount?.companyName ?? null,
|
||||
status: row.status,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma, CityPartnerScopeType, CityPartnerStatus } from '@prisma/client';
|
||||
import { resolveOrderCityPartner, validatePartnerCityBinding } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
const PRIMARY_WHERE = { isPrimary: 1 } as const;
|
||||
|
||||
@Injectable()
|
||||
export class PartnerCityService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async listByCity(cityId: bigint) {
|
||||
const rows = await this.prisma.partnerAccount.findMany({
|
||||
where: { ...PRIMARY_WHERE, cityId },
|
||||
include: { city: { select: { id: true, code: true, name: true } } },
|
||||
orderBy: [{ scopeType: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
return rows.map((row) => this.toDto(row));
|
||||
}
|
||||
|
||||
async listCityIdsForPartnerAccount(partnerAccountId: bigint): Promise<bigint[]> {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.cityId) return [];
|
||||
return [primary.cityId];
|
||||
}
|
||||
|
||||
async assertPartnerAccountBoundToCity(partnerAccountId: bigint, cityId: bigint) {
|
||||
const row = await this.prisma.partnerAccount.findFirst({
|
||||
where: {
|
||||
id: partnerAccountId,
|
||||
...PRIMARY_WHERE,
|
||||
cityId,
|
||||
bindingStatus: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
if (!row) {
|
||||
throw new BadRequestException('合伙人未绑定该开城城市');
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
async assertPartnerBoundToCity(partnerAccountId: bigint, cityId: bigint) {
|
||||
return this.assertPartnerAccountBoundToCity(partnerAccountId, cityId);
|
||||
}
|
||||
|
||||
async resolveForOrder(cityId: bigint, receiverDistrict?: string | null) {
|
||||
const bindings = await this.prisma.partnerAccount.findMany({
|
||||
where: { ...PRIMARY_WHERE, cityId, bindingStatus: 'ACTIVE' },
|
||||
});
|
||||
const ref = resolveOrderCityPartner(
|
||||
bindings.map((b) => ({
|
||||
id: b.id.toString(),
|
||||
partnerAccountId: b.id.toString(),
|
||||
scopeType: b.scopeType as CityPartnerScopeType,
|
||||
districtCodes: this.parseDistrictCodes(b.districtCodes),
|
||||
orderCommissionRate: Number(b.orderCommissionRate ?? 0),
|
||||
redeemCommissionRate: Number(b.redeemCommissionRate ?? 0.03),
|
||||
bindingStatus: b.bindingStatus as CityPartnerStatus,
|
||||
})),
|
||||
receiverDistrict,
|
||||
);
|
||||
if (!ref) return null;
|
||||
return {
|
||||
partnerAccountId: BigInt(ref.partnerAccountId),
|
||||
orderCommissionRate: ref.orderCommissionRate,
|
||||
redeemCommissionRate: ref.redeemCommissionRate,
|
||||
};
|
||||
}
|
||||
|
||||
async validatePrimaryBinding(
|
||||
cityId: bigint,
|
||||
input: {
|
||||
partnerAccountId?: string;
|
||||
scopeType: CityPartnerScopeType;
|
||||
districtCodes?: string[];
|
||||
},
|
||||
excludeId?: bigint,
|
||||
) {
|
||||
const existing = await this.prisma.partnerAccount.findMany({
|
||||
where: { ...PRIMARY_WHERE, cityId, ...(excludeId ? { NOT: { id: excludeId } } : {}) },
|
||||
});
|
||||
const validation = validatePartnerCityBinding(
|
||||
existing.map((r) => ({
|
||||
id: r.id.toString(),
|
||||
partnerAccountId: r.id.toString(),
|
||||
scopeType: r.scopeType as CityPartnerScopeType,
|
||||
districtCodes: this.parseDistrictCodes(r.districtCodes),
|
||||
})),
|
||||
{
|
||||
partnerAccountId: input.partnerAccountId ?? 'new',
|
||||
scopeType: input.scopeType,
|
||||
districtCodes: input.districtCodes,
|
||||
},
|
||||
excludeId?.toString(),
|
||||
);
|
||||
if (!validation.ok) throw new BadRequestException(validation.message);
|
||||
}
|
||||
|
||||
async buildPartnerOrderWhere(partnerAccountId: bigint): Promise<Prisma.OrderWhereInput> {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.cityId) return { id: -1n };
|
||||
return { cityId: primary.cityId };
|
||||
}
|
||||
|
||||
async buildPartnerCityWhere(partnerAccountId: bigint): Promise<Prisma.CommonCityWhereInput> {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccountId);
|
||||
if (!primary.cityId) return { id: -1n };
|
||||
return { id: primary.cityId };
|
||||
}
|
||||
|
||||
async resolvePrimaryAccount(accountId: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUnique({ where: { id: accountId } });
|
||||
if (!account) throw new NotFoundException('合伙人账号不存在');
|
||||
if (account.isPrimary === 1) return account;
|
||||
if (!account.parentAccountId) {
|
||||
throw new BadRequestException('子账号缺少主账号');
|
||||
}
|
||||
return this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: account.parentAccountId } });
|
||||
}
|
||||
|
||||
parseDistrictCodes(value: Prisma.JsonValue | null): string[] | null {
|
||||
if (!value || !Array.isArray(value)) return null;
|
||||
return value.map((v) => String(v));
|
||||
}
|
||||
|
||||
toDto(row: {
|
||||
id: bigint;
|
||||
cityId: bigint | null;
|
||||
companyName: string | null;
|
||||
phone: string;
|
||||
name: string;
|
||||
scopeType: string | null;
|
||||
districtCodes: Prisma.JsonValue | null;
|
||||
orderCommissionRate: Prisma.Decimal | null;
|
||||
redeemCommissionRate: Prisma.Decimal | null;
|
||||
bindingStatus: string | null;
|
||||
managedWarehouseId: bigint | null;
|
||||
status: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
city?: { id: bigint; code: string; name: string } | null;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id.toString(),
|
||||
cityId: row.cityId?.toString() ?? null,
|
||||
cityName: row.city?.name ?? null,
|
||||
cityCode: row.city?.code ?? null,
|
||||
companyName: row.companyName,
|
||||
phone: row.phone,
|
||||
name: row.name,
|
||||
scopeType: row.scopeType,
|
||||
districtCodes: this.parseDistrictCodes(row.districtCodes),
|
||||
orderCommissionRate: Number(row.orderCommissionRate ?? 0),
|
||||
redeemCommissionRate: Number(row.redeemCommissionRate ?? 0.03),
|
||||
bindingStatus: row.bindingStatus,
|
||||
managedWarehouseId: row.managedWarehouseId?.toString() ?? null,
|
||||
status: row.status,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -168,15 +168,15 @@ export class AuthService {
|
||||
}
|
||||
|
||||
private trackPartnerEvent(
|
||||
partnerAccountId: bigint | undefined,
|
||||
partnerId: bigint,
|
||||
actorAccountId: bigint | undefined,
|
||||
primaryAccountId: bigint,
|
||||
clientApp: ClientApp | string,
|
||||
eventName: string,
|
||||
extraJson?: Record<string, unknown>,
|
||||
ref?: { refType?: string; refId?: bigint },
|
||||
) {
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, clientApp, {
|
||||
partnerId,
|
||||
this.analyticsService.trackPartnerOneSafe(actorAccountId, clientApp, {
|
||||
partnerAccountId: primaryAccountId,
|
||||
eventName,
|
||||
refType: ref?.refType,
|
||||
refId: ref?.refId,
|
||||
@@ -184,10 +184,42 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
private async resolvePrimaryAccount(accountId: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUnique({ where: { id: accountId } });
|
||||
if (!account) throw new NotFoundException('合伙人账号不存在');
|
||||
if (account.isPrimary === 1) return account;
|
||||
if (!account.parentAccountId) {
|
||||
throw new BadRequestException('子账号缺少主账号');
|
||||
}
|
||||
return this.prisma.partnerAccount.findUniqueOrThrow({ where: { id: account.parentAccountId } });
|
||||
}
|
||||
|
||||
private partnerTokenPayload(
|
||||
account: {
|
||||
id: bigint;
|
||||
name: string;
|
||||
phone: string;
|
||||
isPrimary: number;
|
||||
staffRole: string | null;
|
||||
permissions?: unknown;
|
||||
},
|
||||
primary: { id: bigint; companyName: string | null },
|
||||
) {
|
||||
return {
|
||||
id: account.id.toString(),
|
||||
primaryAccountId: primary.id.toString(),
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
staffRole: account.staffRole ?? undefined,
|
||||
companyName: primary.companyName ?? undefined,
|
||||
permissions: Array.isArray(account.permissions) ? account.permissions : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private async assertPartnerAccountByPhone(phone: string) {
|
||||
const account = await this.prisma.partnerAccount.findUnique({
|
||||
where: { phone },
|
||||
include: { partner: true },
|
||||
});
|
||||
if (!account) throw new BadRequestException('未找到合伙人账号');
|
||||
if (account.status !== 'ACTIVE') throw new BadRequestException('合伙人账号已停用');
|
||||
@@ -197,11 +229,12 @@ export class AuthService {
|
||||
async checkPartnerPhone(phone: string) {
|
||||
const normalizedPhone = this.assertMobilePhone(phone);
|
||||
const account = await this.assertPartnerAccountByPhone(normalizedPhone);
|
||||
const primary = await this.resolvePrimaryAccount(account.id);
|
||||
return {
|
||||
ok: true,
|
||||
maskedPhone: this.maskPhone(normalizedPhone),
|
||||
name: account.name,
|
||||
companyName: account.partner.companyName,
|
||||
companyName: primary.companyName,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -306,12 +339,13 @@ export class AuthService {
|
||||
) {
|
||||
const partnerAccount = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: actorRef.refId },
|
||||
select: { id: true, partnerId: true },
|
||||
select: { id: true, isPrimary: true, parentAccountId: true },
|
||||
});
|
||||
if (partnerAccount) {
|
||||
const primary = await this.resolvePrimaryAccount(partnerAccount.id);
|
||||
this.trackPartnerEvent(
|
||||
partnerAccount.id,
|
||||
partnerAccount.partnerId,
|
||||
primary.id,
|
||||
clientApp,
|
||||
'partner_sms_send',
|
||||
{
|
||||
@@ -415,20 +449,12 @@ export class AuthService {
|
||||
private async buildPartnerSessionResponse(accountId: bigint, clientApp: ClientApp) {
|
||||
const account = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: accountId },
|
||||
include: { partner: true },
|
||||
});
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
return this.issueToken('PARTNER', account.id, clientApp, false, undefined, undefined, {
|
||||
id: account.id.toString(),
|
||||
partnerId: account.partnerId.toString(),
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
staffRole: account.staffRole ?? undefined,
|
||||
companyName: account.partner.companyName,
|
||||
});
|
||||
const primary = await this.resolvePrimaryAccount(account.id);
|
||||
return this.issueToken('PARTNER', account.id, clientApp, false, undefined, undefined, this.partnerTokenPayload(account, primary));
|
||||
}
|
||||
|
||||
async loginUser(phone: string, code: string, clientApp: ClientApp, guestId?: bigint) {
|
||||
@@ -601,7 +627,8 @@ export class AuthService {
|
||||
} catch (err) {
|
||||
const account = await this.prisma.partnerAccount.findUnique({ where: { phone: normalizedPhone } });
|
||||
if (account) {
|
||||
this.trackPartnerEvent(account.id, account.partnerId, clientApp, 'partner_sms_verify_fail', {
|
||||
const primary = await this.resolvePrimaryAccount(account.id);
|
||||
this.trackPartnerEvent(account.id, primary.id, clientApp, 'partner_sms_verify_fail', {
|
||||
phone: this.maskPhone(normalizedPhone),
|
||||
reason: err instanceof BadRequestException ? err.message : '验证码错误',
|
||||
});
|
||||
@@ -610,29 +637,21 @@ export class AuthService {
|
||||
}
|
||||
const account = await this.prisma.partnerAccount.findUnique({
|
||||
where: { phone: normalizedPhone },
|
||||
include: { partner: true },
|
||||
});
|
||||
if (!account) throw new BadRequestException('未找到合伙人账号');
|
||||
if (account.status !== 'ACTIVE') throw new BadRequestException('合伙人账号已停用');
|
||||
const primary = await this.resolvePrimaryAccount(account.id);
|
||||
await this.prisma.partnerAccount.update({
|
||||
where: { id: account.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
this.trackPartnerEvent(account.id, account.partnerId, clientApp, 'partner_sms_login', {
|
||||
this.trackPartnerEvent(account.id, primary.id, clientApp, 'partner_sms_login', {
|
||||
phone: this.maskPhone(normalizedPhone),
|
||||
});
|
||||
this.trackPartnerEvent(account.id, account.partnerId, clientApp, 'partner_login_success', {
|
||||
this.trackPartnerEvent(account.id, primary.id, clientApp, 'partner_login_success', {
|
||||
method: 'sms',
|
||||
});
|
||||
return this.issueToken('PARTNER', account.id, clientApp, false, undefined, undefined, {
|
||||
id: account.id.toString(),
|
||||
partnerId: account.partnerId.toString(),
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
staffRole: account.staffRole ?? undefined,
|
||||
companyName: account.partner.companyName,
|
||||
});
|
||||
return this.issueToken('PARTNER', account.id, clientApp, false, undefined, undefined, this.partnerTokenPayload(account, primary));
|
||||
}
|
||||
|
||||
async loginHq(phone: string, code: string, clientApp: ClientApp) {
|
||||
@@ -734,9 +753,14 @@ export class AuthService {
|
||||
if (actorType === 'PARTNER') {
|
||||
const account = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: actorId },
|
||||
include: { partner: true },
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
if (!account) return null;
|
||||
const primary = await this.resolvePrimaryAccount(account.id);
|
||||
return serializeBigInt({
|
||||
...account,
|
||||
primaryAccountId: primary.id,
|
||||
companyName: primary.companyName,
|
||||
});
|
||||
}
|
||||
if (actorType === 'HQ') {
|
||||
const account = await this.prisma.hqAccount.findUnique({ where: { id: actorId } });
|
||||
@@ -1102,7 +1126,6 @@ export class AuthService {
|
||||
|
||||
const account = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: partnerAccountId },
|
||||
include: { partner: true },
|
||||
});
|
||||
if (!account) throw new BadRequestException('合伙人账号不存在');
|
||||
|
||||
@@ -1120,19 +1143,12 @@ export class AuthService {
|
||||
wxUnionId: session.unionId ?? account.wxUnionId,
|
||||
lastLoginAt: new Date(),
|
||||
},
|
||||
include: { partner: true },
|
||||
});
|
||||
const primary = await this.resolvePrimaryAccount(updated.id);
|
||||
|
||||
this.trackPartnerEvent(updated.id, updated.partnerId, clientApp, 'partner_wechat_bind', { platform });
|
||||
this.trackPartnerEvent(updated.id, primary.id, clientApp, 'partner_wechat_bind', { platform });
|
||||
|
||||
return this.issueToken('PARTNER', updated.id, clientApp, false, undefined, undefined, {
|
||||
id: updated.id.toString(),
|
||||
partnerId: updated.partnerId.toString(),
|
||||
name: updated.name,
|
||||
phone: updated.phone,
|
||||
isPrimary: updated.isPrimary === 1,
|
||||
companyName: updated.partner.companyName,
|
||||
});
|
||||
return this.issueToken('PARTNER', updated.id, clientApp, false, undefined, undefined, this.partnerTokenPayload(updated, primary));
|
||||
}
|
||||
|
||||
async loginPartnerWechat(code: string, clientApp: ClientApp, platform: 'h5' | 'mini' = 'h5') {
|
||||
@@ -1144,7 +1160,6 @@ export class AuthService {
|
||||
|
||||
let account = await this.prisma.partnerAccount.findFirst({
|
||||
where: { wxOpenId: session.openId },
|
||||
include: { partner: true },
|
||||
});
|
||||
|
||||
if (!account && this.wechatProvider.isMock()) {
|
||||
@@ -1152,7 +1167,6 @@ export class AuthService {
|
||||
account = await this.prisma.partnerAccount.findFirst({
|
||||
where: { status: 'ACTIVE' },
|
||||
orderBy: [{ isPrimary: 'desc' }, { id: 'asc' }],
|
||||
include: { partner: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1167,23 +1181,15 @@ export class AuthService {
|
||||
wxUnionId: session.unionId ?? account.wxUnionId,
|
||||
lastLoginAt: new Date(),
|
||||
},
|
||||
include: { partner: true },
|
||||
});
|
||||
const primary = await this.resolvePrimaryAccount(account.id);
|
||||
|
||||
this.trackPartnerEvent(account.id, account.partnerId, clientApp, 'partner_wechat_login', { platform });
|
||||
this.trackPartnerEvent(account.id, account.partnerId, clientApp, 'partner_login_success', {
|
||||
this.trackPartnerEvent(account.id, primary.id, clientApp, 'partner_wechat_login', { platform });
|
||||
this.trackPartnerEvent(account.id, primary.id, clientApp, 'partner_login_success', {
|
||||
method: 'wechat',
|
||||
});
|
||||
|
||||
return this.issueToken('PARTNER', account.id, clientApp, false, undefined, undefined, {
|
||||
id: account.id.toString(),
|
||||
partnerId: account.partnerId.toString(),
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
staffRole: account.staffRole ?? undefined,
|
||||
companyName: account.partner.companyName,
|
||||
});
|
||||
return this.issueToken('PARTNER', account.id, clientApp, false, undefined, undefined, this.partnerTokenPayload(account, primary));
|
||||
}
|
||||
|
||||
private async mergeUsers(guestId: bigint, primaryId: bigint): Promise<UserRow> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { IsArray, IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
import { AccountStatus, PartnerStaffRole } from '@dukang/shared-types';
|
||||
|
||||
export class CreatePartnerStaffDto {
|
||||
@@ -15,6 +15,11 @@ export class CreatePartnerStaffDto {
|
||||
@IsString()
|
||||
@IsIn(Object.values(PartnerStaffRole))
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class UpdatePartnerStaffDto {
|
||||
@@ -27,6 +32,11 @@ export class UpdatePartnerStaffDto {
|
||||
@IsOptional()
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(AccountStatus))
|
||||
@IsOptional()
|
||||
|
||||
@@ -17,7 +17,7 @@ export class PartnerStaffController {
|
||||
|
||||
@Post()
|
||||
create(@CurrentUser() user: AuthUser, @Body() dto: CreatePartnerStaffDto) {
|
||||
return this.staffService.createStaff(user.actorId, dto);
|
||||
return this.staffService.createStaff(user, dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@@ -26,11 +26,11 @@ export class PartnerStaffController {
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdatePartnerStaffDto,
|
||||
) {
|
||||
return this.staffService.updateStaff(user.actorId, BigInt(id), dto);
|
||||
return this.staffService.updateStaff(user, BigInt(id), dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||
return this.staffService.deleteStaff(user.actorId, BigInt(id));
|
||||
return this.staffService.deleteStaff(user, BigInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +1,182 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PartnerStaffRole } from '@dukang/shared-types';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PartnerStaffService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async listStaff(parentAccountId: bigint) {
|
||||
const rows = await this.prisma.partnerAccount.findMany({
|
||||
where: { parentAccountId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return rows.map((row) => this.toStaffItem(row));
|
||||
}
|
||||
|
||||
async createStaff(parentAccountId: bigint, dto: CreatePartnerStaffDto) {
|
||||
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 name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: parent.partnerId,
|
||||
phone,
|
||||
name,
|
||||
staffRole: (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL,
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
status: 'DISABLED',
|
||||
},
|
||||
});
|
||||
|
||||
return this.toStaffItem(account);
|
||||
}
|
||||
|
||||
async updateStaff(parentAccountId: bigint, staffId: bigint, dto: UpdatePartnerStaffDto) {
|
||||
const staff = await this.assertStaffOwned(parentAccountId, 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 as PartnerStaffRole;
|
||||
}
|
||||
if (dto.status !== undefined) {
|
||||
data.status = dto.status;
|
||||
}
|
||||
const updated = await this.prisma.partnerAccount.update({
|
||||
where: { id: staff.id },
|
||||
data,
|
||||
});
|
||||
return this.toStaffItem(updated);
|
||||
}
|
||||
|
||||
async deleteStaff(parentAccountId: bigint, staffId: bigint) {
|
||||
const staff = await this.assertStaffOwned(parentAccountId, staffId);
|
||||
await this.prisma.partnerAccount.delete({ where: { id: staff.id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
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;
|
||||
status: string;
|
||||
lastLoginAt: Date | null;
|
||||
}) {
|
||||
return serializeBigInt({
|
||||
id: row.id.toString(),
|
||||
name: row.name,
|
||||
phone: this.maskPhone(row.phone),
|
||||
staffRole: row.staffRole ?? PartnerStaffRole.INTERNAL,
|
||||
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)}`;
|
||||
}
|
||||
}
|
||||
import {
|
||||
|
||||
BadRequestException,
|
||||
|
||||
Injectable,
|
||||
|
||||
NotFoundException,
|
||||
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { PartnerStaffRole } 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 { CreatePartnerStaffDto, UpdatePartnerStaffDto } from './dto/partner-staff.dto';
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
|
||||
export class PartnerStaffService {
|
||||
|
||||
constructor(
|
||||
|
||||
private readonly prisma: PrismaService,
|
||||
|
||||
private readonly analytics: AnalyticsService,
|
||||
|
||||
) {}
|
||||
|
||||
|
||||
|
||||
async listStaff(parentAccountId: bigint) {
|
||||
|
||||
const rows = await this.prisma.partnerAccount.findMany({
|
||||
|
||||
where: { parentAccountId },
|
||||
|
||||
orderBy: { createdAt: 'desc' },
|
||||
|
||||
});
|
||||
|
||||
return rows.map((row) => this.toStaffItem(row));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 name = dto.name.trim();
|
||||
|
||||
if (!name) throw new BadRequestException('请填写真实姓名');
|
||||
|
||||
|
||||
|
||||
const staffRole = (dto.staffRole as PartnerStaffRole | undefined) ?? PartnerStaffRole.INTERNAL;
|
||||
|
||||
|
||||
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
|
||||
data: {
|
||||
|
||||
phone,
|
||||
|
||||
name,
|
||||
|
||||
staffRole,
|
||||
|
||||
permissions: dto.permissions ?? undefined,
|
||||
|
||||
isPrimary: 0,
|
||||
|
||||
parentAccountId: parent.id,
|
||||
|
||||
status: 'DISABLED',
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
this.trackStaffEvent(actor, parent.id, 'partner_staff_create', account.id, {
|
||||
|
||||
name,
|
||||
|
||||
phone: this.maskPhone(phone),
|
||||
|
||||
staffRole,
|
||||
|
||||
status: account.status,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { resolveMaxPartnerCommissionRate, validatePartnerCommissionRates } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import type { AdminCitiesQueryDto } from './dto/admin-query.dto';
|
||||
import type { CreateCityDto, UpdateCityDto } from './dto/admin-mutate.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminCitiesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async list(query: AdminCitiesQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
@@ -16,7 +21,9 @@ export class AdminCitiesService {
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.code) where.code = { contains: query.code };
|
||||
if (query.status) where.status = query.status as Prisma.EnumCityStatusFilter['equals'];
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.partnerId) {
|
||||
where.partnerAccounts = { some: { id: BigInt(query.partnerId), isPrimary: 1 } };
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.commonCity.findMany({
|
||||
@@ -25,8 +32,12 @@ export class AdminCitiesService {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
partner: { select: { id: true, companyName: true } },
|
||||
_count: { select: { stores: true, orders: true } },
|
||||
partnerAccounts: {
|
||||
where: { isPrimary: 1 },
|
||||
select: { id: true, companyName: true, scopeType: true, bindingStatus: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
_count: { select: { stores: true, orders: true, partnerAccounts: true, warehouses: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.commonCity.count({ where }),
|
||||
@@ -34,8 +45,18 @@ export class AdminCitiesService {
|
||||
return serializeBigInt({
|
||||
items: items.map((c) => ({
|
||||
...c,
|
||||
partnerBindings: c.partnerAccounts.map((bp) => ({
|
||||
id: bp.id.toString(),
|
||||
partnerAccountId: bp.id.toString(),
|
||||
partnerCompanyName: bp.companyName,
|
||||
scopeType: bp.scopeType,
|
||||
status: bp.bindingStatus,
|
||||
})),
|
||||
partnerAccounts: undefined,
|
||||
storeCount: c._count.stores,
|
||||
orderCount: c._count.orders,
|
||||
partnerBindingCount: c.partnerAccounts.length,
|
||||
warehouseCount: c._count.warehouses,
|
||||
_count: undefined,
|
||||
})),
|
||||
total,
|
||||
@@ -48,13 +69,22 @@ export class AdminCitiesService {
|
||||
const city = await this.prisma.commonCity.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
partner: true,
|
||||
commissionRule: true,
|
||||
warehouses: {
|
||||
include: { partnerAccount: { select: { id: true, companyName: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
},
|
||||
_count: { select: { stores: true, orders: true } },
|
||||
},
|
||||
});
|
||||
if (!city) throw new NotFoundException('开城城市不存在');
|
||||
return serializeBigInt(city);
|
||||
const cityPartners = await this.partnerCityService.listByCity(id);
|
||||
return serializeBigInt({
|
||||
...city,
|
||||
cityPartners,
|
||||
storeCount: city._count.stores,
|
||||
orderCount: city._count.orders,
|
||||
_count: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async create(dto: CreateCityDto) {
|
||||
@@ -65,31 +95,48 @@ export class AdminCitiesService {
|
||||
code: dto.code,
|
||||
name: dto.name,
|
||||
province: dto.province,
|
||||
partnerId: dto.partnerId ? BigInt(dto.partnerId) : null,
|
||||
status: (dto.status ?? 'PENDING') as 'PENDING' | 'ACTIVE' | 'PAUSED',
|
||||
commissionRule: {
|
||||
create: {
|
||||
orderCommissionRate: 0,
|
||||
redeemCommissionRate: 0.03,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return serializeBigInt(city);
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateCityDto) {
|
||||
if (dto.maxPartnerCommissionRate !== undefined) {
|
||||
const maxRate = resolveMaxPartnerCommissionRate(dto.maxPartnerCommissionRate);
|
||||
const partners = await this.prisma.partnerAccount.findMany({
|
||||
where: { cityId: id, isPrimary: 1 },
|
||||
select: {
|
||||
companyName: true,
|
||||
orderCommissionRate: true,
|
||||
redeemCommissionRate: true,
|
||||
},
|
||||
});
|
||||
for (const partner of partners) {
|
||||
const check = validatePartnerCommissionRates(
|
||||
Number(partner.orderCommissionRate ?? 0),
|
||||
Number(partner.redeemCommissionRate ?? 0.03),
|
||||
maxRate,
|
||||
);
|
||||
if (!check.ok) {
|
||||
throw new BadRequestException(
|
||||
`无法保存:合伙人「${partner.companyName ?? '—'}」${check.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const city = await this.prisma.commonCity.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.province !== undefined ? { province: dto.province } : {}),
|
||||
...(dto.partnerId !== undefined
|
||||
? { partnerId: dto.partnerId ? BigInt(dto.partnerId) : null }
|
||||
: {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'PENDING' | 'ACTIVE' | 'PAUSED' } : {}),
|
||||
...(dto.localMinQty !== undefined ? { localMinQty: dto.localMinQty } : {}),
|
||||
...(dto.crossMinQty !== undefined ? { crossMinQty: dto.crossMinQty } : {}),
|
||||
...(dto.maxPartnerCommissionRate !== undefined
|
||||
? { maxPartnerCommissionRate: dto.maxPartnerCommissionRate }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
return serializeBigInt(city);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
import { CityWarehouseService } from '../city-scope/city-warehouse.service';
|
||||
import { CreateCityWarehouseDto, UpdateCityWarehouseDto } from './dto/admin-mutate.dto';
|
||||
import { AdminCityWarehousesQueryDto } from './dto/admin-query.dto';
|
||||
import type { WarehouseManagerType, WarehouseStatus } from '@prisma/client';
|
||||
|
||||
@Controller('admin/cities/:cityId/warehouses')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminCityWarehousesController {
|
||||
constructor(private readonly service: CityWarehouseService) {}
|
||||
|
||||
@Get()
|
||||
list(@Param('cityId') cityId: string) {
|
||||
return this.service.listByCity(BigInt(cityId));
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WAREHOUSE_CREATE,
|
||||
refType: 'WAREHOUSE',
|
||||
refIdField: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
create(@Param('cityId') cityId: string, @Body() dto: CreateCityWarehouseDto) {
|
||||
return this.service.create(BigInt(cityId), {
|
||||
name: dto.name,
|
||||
address: dto.address,
|
||||
contactName: dto.contactName,
|
||||
contactPhone: dto.contactPhone,
|
||||
managerType: dto.managerType as WarehouseManagerType,
|
||||
partnerAccountId: dto.partnerAccountId ? BigInt(dto.partnerAccountId) : undefined,
|
||||
status: dto.status as WarehouseStatus | undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('admin/city-warehouses')
|
||||
@UseGuards(HqAuthGuard)
|
||||
export class AdminCityWarehouseMutationsController {
|
||||
constructor(private readonly service: CityWarehouseService) {}
|
||||
|
||||
@Get()
|
||||
listAll(@Query() query: AdminCityWarehousesQueryDto) {
|
||||
return this.service.listAll(query);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WAREHOUSE_UPDATE,
|
||||
refType: 'WAREHOUSE',
|
||||
refIdParam: 'id',
|
||||
includeBody: true,
|
||||
})
|
||||
update(@Param('id') id: string, @Body() dto: UpdateCityWarehouseDto) {
|
||||
return this.service.update(BigInt(id), {
|
||||
name: dto.name,
|
||||
address: dto.address,
|
||||
contactName: dto.contactName,
|
||||
contactPhone: dto.contactPhone,
|
||||
managerType: dto.managerType as WarehouseManagerType | undefined,
|
||||
partnerAccountId:
|
||||
dto.partnerAccountId === null
|
||||
? undefined
|
||||
: dto.partnerAccountId
|
||||
? BigInt(dto.partnerAccountId)
|
||||
: undefined,
|
||||
status: dto.status as WarehouseStatus | undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.WAREHOUSE_DELETE,
|
||||
refType: 'WAREHOUSE',
|
||||
refIdParam: 'id',
|
||||
})
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.remove(BigInt(id));
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export class AdminDashboardService {
|
||||
_count: { status: true },
|
||||
}),
|
||||
this.prisma.store.count(),
|
||||
this.prisma.partner.count(),
|
||||
this.prisma.partnerAccount.count({ where: { isPrimary: 1 } }),
|
||||
this.prisma.redeemRecord.count({ where: { createdAt: { gte: todayStart } } }),
|
||||
this.prisma.orderDelivery.count(),
|
||||
this.prisma.storePayout.count({ where: { status: 'PENDING' } }),
|
||||
|
||||
@@ -15,12 +15,12 @@ export class AdminPartnerLogsService {
|
||||
async list(query: AdminPartnerLogsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const partnerIds = await this.resolvePartnerIds(query);
|
||||
if (partnerIds && partnerIds.length === 0) {
|
||||
const partnerAccountIds = await this.resolvePartnerAccountIds(query);
|
||||
if (partnerAccountIds && partnerAccountIds.length === 0) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
|
||||
const where = this.buildWhere(query, partnerIds);
|
||||
const where = this.buildWhere(query, partnerAccountIds);
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.logPartnerAnalytics.findMany({
|
||||
where,
|
||||
@@ -44,10 +44,10 @@ export class AdminPartnerLogsService {
|
||||
|
||||
private buildWhere(
|
||||
query: AdminPartnerLogsQueryDto,
|
||||
partnerIds?: bigint[],
|
||||
partnerAccountIds?: bigint[],
|
||||
): Prisma.LogPartnerAnalyticsWhereInput {
|
||||
const where: Prisma.LogPartnerAnalyticsWhereInput = {};
|
||||
if (partnerIds) where.partnerId = { in: partnerIds };
|
||||
if (partnerAccountIds) where.partnerAccountId = { in: partnerAccountIds };
|
||||
if (query.partnerAccountId) where.partnerAccountId = BigInt(query.partnerAccountId);
|
||||
const categoryEvents = query.eventName
|
||||
? [query.eventName]
|
||||
@@ -64,40 +64,23 @@ export class AdminPartnerLogsService {
|
||||
return where;
|
||||
}
|
||||
|
||||
private async resolvePartnerIds(query: AdminPartnerLogsQueryDto): Promise<bigint[] | undefined> {
|
||||
private async resolvePartnerAccountIds(
|
||||
query: AdminPartnerLogsQueryDto,
|
||||
): Promise<bigint[] | undefined> {
|
||||
if (query.partnerAccountId) return [BigInt(query.partnerAccountId)];
|
||||
if (query.partnerId) return [BigInt(query.partnerId)];
|
||||
|
||||
const partnerWhere: Prisma.PartnerWhereInput = {};
|
||||
if (query.companyName) partnerWhere.companyName = { contains: query.companyName };
|
||||
const accountWhere: Prisma.PartnerAccountWhereInput = { isPrimary: 1 };
|
||||
if (query.companyName) accountWhere.companyName = { contains: query.companyName };
|
||||
if (query.phone) accountWhere.phone = { contains: query.phone };
|
||||
|
||||
if (query.partnerAccountId || query.phone) {
|
||||
const accountWhere: Prisma.PartnerAccountWhereInput = {};
|
||||
if (query.partnerAccountId) accountWhere.id = BigInt(query.partnerAccountId);
|
||||
if (query.phone) accountWhere.phone = { contains: query.phone };
|
||||
if (query.companyName || query.phone) {
|
||||
const accounts = await this.prisma.partnerAccount.findMany({
|
||||
where: accountWhere,
|
||||
select: { partnerId: true },
|
||||
take: 100,
|
||||
});
|
||||
if (accounts.length === 0) return [];
|
||||
const ids = [...new Set(accounts.map((a) => a.partnerId))];
|
||||
if (query.companyName) {
|
||||
const partners = await this.prisma.partner.findMany({
|
||||
where: { id: { in: ids }, ...partnerWhere },
|
||||
select: { id: true },
|
||||
});
|
||||
return partners.map((p) => p.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
if (query.companyName) {
|
||||
const partners = await this.prisma.partner.findMany({
|
||||
where: partnerWhere,
|
||||
select: { id: true },
|
||||
take: 100,
|
||||
});
|
||||
return partners.map((p) => p.id);
|
||||
return accounts.map((a) => a.id);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -106,8 +89,7 @@ export class AdminPartnerLogsService {
|
||||
private async enrichRows(
|
||||
rows: Array<{
|
||||
id: bigint;
|
||||
partnerAccountId: bigint | null;
|
||||
partnerId: bigint;
|
||||
partnerAccountId: bigint;
|
||||
eventName: string;
|
||||
clientApp: string | null;
|
||||
refType: string | null;
|
||||
@@ -116,37 +98,25 @@ export class AdminPartnerLogsService {
|
||||
createdAt: Date;
|
||||
}>,
|
||||
) {
|
||||
const partnerIds = [...new Set(rows.map((r) => r.partnerId))];
|
||||
const accountIds = [...new Set(rows.map((r) => r.partnerAccountId).filter((id): id is bigint => id != null))];
|
||||
const accountIds = [...new Set(rows.map((r) => r.partnerAccountId))];
|
||||
const accounts = accountIds.length
|
||||
? await this.prisma.partnerAccount.findMany({
|
||||
where: { id: { in: accountIds } },
|
||||
select: { id: true, name: true, phone: true, companyName: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const [partners, accounts] = await Promise.all([
|
||||
partnerIds.length
|
||||
? this.prisma.partner.findMany({
|
||||
where: { id: { in: partnerIds } },
|
||||
select: { id: true, companyName: true },
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
accountIds.length
|
||||
? this.prisma.partnerAccount.findMany({
|
||||
where: { id: { in: accountIds } },
|
||||
select: { id: true, name: true, phone: true },
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const partnerMap = new Map(partners.map((p) => [p.id.toString(), p] as const));
|
||||
const accountMap = new Map(accounts.map((a) => [a.id.toString(), a] as const));
|
||||
|
||||
return rows.map((row) => {
|
||||
const partner = partnerMap.get(row.partnerId.toString());
|
||||
const account = row.partnerAccountId ? accountMap.get(row.partnerAccountId.toString()) : undefined;
|
||||
const account = accountMap.get(row.partnerAccountId.toString());
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
partnerId: row.partnerId.toString(),
|
||||
partnerAccountId: row.partnerAccountId?.toString() ?? null,
|
||||
partnerId: row.partnerAccountId.toString(),
|
||||
partnerAccountId: row.partnerAccountId.toString(),
|
||||
accountName: account?.name ?? null,
|
||||
accountPhone: account?.phone ?? null,
|
||||
companyName: partner?.companyName ?? null,
|
||||
companyName: account?.companyName ?? null,
|
||||
category: resolvePartnerLogCategory(row.eventName),
|
||||
eventName: row.eventName,
|
||||
clientApp: row.clientApp,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Prisma, CityPartnerScopeType, CityPartnerStatus } from '@prisma/client';
|
||||
import { resolveMaxPartnerCommissionRate, validatePartnerCommissionRates } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import type { AdminPartnerAccountsQueryDto, AdminPartnersQueryDto } from './dto/admin-query.dto';
|
||||
import type {
|
||||
CreatePartnerAccountDto,
|
||||
@@ -10,77 +12,36 @@ import type {
|
||||
UpdatePartnerDto,
|
||||
} from './dto/admin-mutate.dto';
|
||||
|
||||
const PRIMARY_WHERE = { isPrimary: 1 } as const;
|
||||
|
||||
function assertPartnerCommissionRates(
|
||||
city: { maxPartnerCommissionRate: Prisma.Decimal | number | null },
|
||||
orderCommissionRate: number,
|
||||
redeemCommissionRate: number,
|
||||
) {
|
||||
const maxRate = resolveMaxPartnerCommissionRate(
|
||||
city.maxPartnerCommissionRate != null ? Number(city.maxPartnerCommissionRate) : null,
|
||||
);
|
||||
const check = validatePartnerCommissionRates(orderCommissionRate, redeemCommissionRate, maxRate);
|
||||
if (!check.ok) throw new BadRequestException(check.message);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminPartnersService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async listPartners(query: AdminPartnersQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.PartnerWhereInput = {};
|
||||
const where: Prisma.PartnerAccountWhereInput = { ...PRIMARY_WHERE };
|
||||
if (query.companyName) where.companyName = { contains: query.companyName };
|
||||
if (query.contactPhone) where.contactPhone = { contains: query.contactPhone };
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partner.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
_count: { select: { stores: true, accounts: true, cities: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.partner.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((p) => ({
|
||||
...p,
|
||||
storeCount: p._count.stores,
|
||||
accountCount: p._count.accounts,
|
||||
cityCount: p._count.cities,
|
||||
_count: undefined,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detailPartner(id: bigint) {
|
||||
const partner = await this.prisma.partner.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
cities: { select: { id: true, code: true, name: true, status: true } },
|
||||
accounts: { select: { id: true, phone: true, name: true, isPrimary: true, status: true } },
|
||||
stores: { select: { id: true, name: true, status: true }, take: 10, orderBy: { createdAt: 'desc' } },
|
||||
_count: { select: { stores: true, accounts: true } },
|
||||
},
|
||||
});
|
||||
if (!partner) throw new NotFoundException('开城合伙人不存在');
|
||||
return serializeBigInt(partner);
|
||||
}
|
||||
|
||||
async createPartner(dto: CreatePartnerDto) {
|
||||
const partner = await this.prisma.partner.create({ data: dto });
|
||||
return serializeBigInt(partner);
|
||||
}
|
||||
|
||||
async updatePartner(id: bigint, dto: UpdatePartnerDto) {
|
||||
const partner = await this.prisma.partner.update({ where: { id }, data: dto });
|
||||
return serializeBigInt(partner);
|
||||
}
|
||||
|
||||
async listPartnerAccounts(query: AdminPartnerAccountsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.PartnerAccountWhereInput = {};
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals'];
|
||||
if (query.isPrimary === '0' || query.isPrimary === '1') {
|
||||
where.isPrimary = Number(query.isPrimary);
|
||||
}
|
||||
if (query.cityId) where.cityId = BigInt(query.cityId);
|
||||
if (query.partnerId) where.id = BigInt(query.partnerId);
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partnerAccount.findMany({
|
||||
@@ -89,8 +50,257 @@ export class AdminPartnersService {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
partner: { select: { id: true, companyName: true } },
|
||||
parent: { select: { id: true, name: true, phone: true } },
|
||||
city: { select: { id: true, code: true, name: true, status: true, maxPartnerCommissionRate: true } },
|
||||
managedWarehouse: { select: { id: true, name: true } },
|
||||
children: {
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
name: true,
|
||||
staffRole: true,
|
||||
permissions: true,
|
||||
status: true,
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
_count: { select: { stores: true, children: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.partnerAccount.count({ where }),
|
||||
]);
|
||||
return serializeBigInt({
|
||||
items: items.map((p) => ({
|
||||
id: p.id.toString(),
|
||||
companyName: p.companyName,
|
||||
contactPhone: p.contactPhone,
|
||||
phone: p.phone,
|
||||
name: p.name,
|
||||
cityId: p.cityId?.toString() ?? null,
|
||||
cityName: p.city?.name ?? null,
|
||||
maxPartnerCommissionRate:
|
||||
p.city?.maxPartnerCommissionRate != null ? Number(p.city.maxPartnerCommissionRate) : null,
|
||||
scopeType: p.scopeType,
|
||||
orderCommissionRate: Number(p.orderCommissionRate ?? 0),
|
||||
redeemCommissionRate: Number(p.redeemCommissionRate ?? 0.03),
|
||||
bindingStatus: p.bindingStatus,
|
||||
managedWarehouseId: p.managedWarehouseId?.toString() ?? null,
|
||||
managedWarehouseName: p.managedWarehouse?.name ?? null,
|
||||
storeCount: p._count.stores,
|
||||
accountCount: p._count.children + 1,
|
||||
children: p.children.map((c) => ({
|
||||
id: c.id.toString(),
|
||||
phone: c.phone,
|
||||
name: c.name,
|
||||
staffRole: c.staffRole,
|
||||
permissions: c.permissions,
|
||||
status: c.status,
|
||||
})),
|
||||
createdAt: p.createdAt,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
async detailPartner(id: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findFirst({
|
||||
where: { id, ...PRIMARY_WHERE },
|
||||
include: {
|
||||
city: { select: { id: true, code: true, name: true, status: true, maxPartnerCommissionRate: true } },
|
||||
managedWarehouse: { select: { id: true, name: true } },
|
||||
children: {
|
||||
where: { isPrimary: 0 },
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
name: true,
|
||||
staffRole: true,
|
||||
permissions: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
stores: { select: { id: true, name: true, status: true }, take: 10, orderBy: { createdAt: 'desc' } },
|
||||
_count: { select: { stores: true, children: true } },
|
||||
},
|
||||
});
|
||||
if (!account) throw new NotFoundException('开城合伙人不存在');
|
||||
return serializeBigInt({
|
||||
...this.partnerCityService.toDto({
|
||||
...account,
|
||||
city: account.city,
|
||||
}),
|
||||
contactPhone: account.contactPhone,
|
||||
address: account.address,
|
||||
bankAccountName: account.bankAccountName,
|
||||
bankAccountNo: account.bankAccountNo,
|
||||
bankBranch: account.bankBranch,
|
||||
managedWarehouseName: account.managedWarehouse?.name ?? null,
|
||||
accountCount: account._count.children + 1,
|
||||
maxPartnerCommissionRate:
|
||||
account.city?.maxPartnerCommissionRate != null
|
||||
? Number(account.city.maxPartnerCommissionRate)
|
||||
: null,
|
||||
children: account.children.map((c) => ({
|
||||
id: c.id.toString(),
|
||||
phone: c.phone,
|
||||
name: c.name,
|
||||
staffRole: c.staffRole,
|
||||
permissions: c.permissions,
|
||||
status: c.status,
|
||||
createdAt: c.createdAt,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async createPartner(dto: CreatePartnerDto) {
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new BadRequestException('请输入正确的登录手机号');
|
||||
}
|
||||
const phoneTaken = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||
if (phoneTaken) throw new BadRequestException('该手机号已被使用');
|
||||
|
||||
const cityId = BigInt(dto.cityId);
|
||||
const city = await this.prisma.commonCity.findUnique({ where: { id: cityId } });
|
||||
if (!city) throw new NotFoundException('开城城市不存在');
|
||||
|
||||
await this.partnerCityService.validatePrimaryBinding(cityId, {
|
||||
scopeType: dto.scopeType as CityPartnerScopeType,
|
||||
districtCodes: dto.districtCodes,
|
||||
});
|
||||
|
||||
const orderCommissionRate = dto.orderCommissionRate ?? 0;
|
||||
const redeemCommissionRate = dto.redeemCommissionRate ?? 0.03;
|
||||
assertPartnerCommissionRates(city, orderCommissionRate, redeemCommissionRate);
|
||||
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
phone,
|
||||
name: dto.name.trim(),
|
||||
isPrimary: 1,
|
||||
staffRole: 'PARTNER',
|
||||
status: 'ACTIVE',
|
||||
cityId,
|
||||
scopeType: dto.scopeType as CityPartnerScopeType,
|
||||
districtCodes:
|
||||
dto.scopeType === 'DISTRICT' ? (dto.districtCodes ?? []) : Prisma.JsonNull,
|
||||
orderCommissionRate: orderCommissionRate,
|
||||
redeemCommissionRate: redeemCommissionRate,
|
||||
bindingStatus: (dto.bindingStatus ?? 'ACTIVE') as CityPartnerStatus,
|
||||
companyName: dto.companyName.trim(),
|
||||
address: dto.address.trim(),
|
||||
contactPhone: dto.contactPhone?.trim() ?? phone,
|
||||
contractNo: dto.contractNo,
|
||||
bankAccountName: dto.bankAccountName,
|
||||
bankAccountNo: dto.bankAccountNo,
|
||||
bankBranch: dto.bankBranch,
|
||||
weeklyStoreTarget: dto.weeklyStoreTarget ?? 20,
|
||||
},
|
||||
include: { city: { select: { id: true, code: true, name: true } } },
|
||||
});
|
||||
|
||||
return serializeBigInt(this.partnerCityService.toDto(account));
|
||||
}
|
||||
|
||||
async updatePartner(id: bigint, dto: UpdatePartnerDto) {
|
||||
const existing = await this.prisma.partnerAccount.findFirst({
|
||||
where: { id, ...PRIMARY_WHERE },
|
||||
});
|
||||
if (!existing) throw new NotFoundException('开城合伙人不存在');
|
||||
|
||||
const city = await this.prisma.commonCity.findUniqueOrThrow({ where: { id: existing.cityId! } });
|
||||
const cityId = existing.cityId!;
|
||||
const scopeType = (dto.scopeType ?? existing.scopeType) as CityPartnerScopeType;
|
||||
const districtCodes =
|
||||
scopeType === 'DISTRICT'
|
||||
? dto.districtCodes ?? this.partnerCityService.parseDistrictCodes(existing.districtCodes)
|
||||
: null;
|
||||
|
||||
await this.partnerCityService.validatePrimaryBinding(
|
||||
cityId,
|
||||
{
|
||||
partnerAccountId: id.toString(),
|
||||
scopeType,
|
||||
districtCodes: districtCodes ?? undefined,
|
||||
},
|
||||
id,
|
||||
);
|
||||
|
||||
if (dto.phone !== undefined) {
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new BadRequestException('请输入正确的登录手机号');
|
||||
}
|
||||
const phoneTaken = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||
if (phoneTaken && phoneTaken.id !== id) {
|
||||
throw new BadRequestException('该手机号已被使用');
|
||||
}
|
||||
}
|
||||
|
||||
const orderCommissionRate =
|
||||
dto.orderCommissionRate !== undefined
|
||||
? dto.orderCommissionRate
|
||||
: Number(existing.orderCommissionRate ?? 0);
|
||||
const redeemCommissionRate =
|
||||
dto.redeemCommissionRate !== undefined
|
||||
? dto.redeemCommissionRate
|
||||
: Number(existing.redeemCommissionRate ?? 0.03);
|
||||
if (dto.orderCommissionRate !== undefined || dto.redeemCommissionRate !== undefined) {
|
||||
assertPartnerCommissionRates(city, orderCommissionRate, redeemCommissionRate);
|
||||
}
|
||||
|
||||
const account = await this.prisma.partnerAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name.trim() } : {}),
|
||||
...(dto.phone !== undefined ? { phone: dto.phone.trim() } : {}),
|
||||
...(dto.companyName !== undefined ? { companyName: dto.companyName.trim() } : {}),
|
||||
...(dto.address !== undefined ? { address: dto.address.trim() } : {}),
|
||||
...(dto.contactPhone !== undefined ? { contactPhone: dto.contactPhone.trim() } : {}),
|
||||
...(dto.scopeType !== undefined ? { scopeType: dto.scopeType as CityPartnerScopeType } : {}),
|
||||
...(dto.scopeType !== undefined || dto.districtCodes !== undefined
|
||||
? {
|
||||
districtCodes:
|
||||
scopeType === 'DISTRICT'
|
||||
? ((districtCodes ?? []) as Prisma.InputJsonValue)
|
||||
: Prisma.JsonNull,
|
||||
}
|
||||
: {}),
|
||||
...(dto.orderCommissionRate !== undefined ? { orderCommissionRate: dto.orderCommissionRate } : {}),
|
||||
...(dto.redeemCommissionRate !== undefined ? { redeemCommissionRate: dto.redeemCommissionRate } : {}),
|
||||
...(dto.bindingStatus !== undefined ? { bindingStatus: dto.bindingStatus as CityPartnerStatus } : {}),
|
||||
...(dto.contractNo !== undefined ? { contractNo: dto.contractNo } : {}),
|
||||
...(dto.bankAccountName !== undefined ? { bankAccountName: dto.bankAccountName } : {}),
|
||||
...(dto.bankAccountNo !== undefined ? { bankAccountNo: dto.bankAccountNo } : {}),
|
||||
...(dto.bankBranch !== undefined ? { bankBranch: dto.bankBranch } : {}),
|
||||
...(dto.weeklyStoreTarget !== undefined ? { weeklyStoreTarget: dto.weeklyStoreTarget } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}),
|
||||
},
|
||||
include: { city: { select: { id: true, code: true, name: true } } },
|
||||
});
|
||||
|
||||
return serializeBigInt(this.partnerCityService.toDto(account));
|
||||
}
|
||||
|
||||
async listPartnerAccounts(query: AdminPartnerAccountsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.PartnerAccountWhereInput = { isPrimary: 0 };
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
if (query.partnerId) where.parentAccountId = BigInt(query.partnerId);
|
||||
if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals'];
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partnerAccount.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
parent: { select: { id: true, name: true, phone: true, companyName: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.partnerAccount.count({ where }),
|
||||
@@ -98,14 +308,14 @@ export class AdminPartnersService {
|
||||
return serializeBigInt({ items, total, page, pageSize });
|
||||
}
|
||||
|
||||
async listPartnerAccountTree(partnerId?: bigint) {
|
||||
const where: Prisma.PartnerAccountWhereInput = {};
|
||||
if (partnerId) where.partnerId = partnerId;
|
||||
async listPartnerAccountTree(primaryAccountId?: bigint) {
|
||||
const where: Prisma.PartnerAccountWhereInput = primaryAccountId
|
||||
? { OR: [{ id: primaryAccountId }, { parentAccountId: primaryAccountId }] }
|
||||
: { isPrimary: 1 };
|
||||
|
||||
const accounts = await this.prisma.partnerAccount.findMany({
|
||||
where,
|
||||
orderBy: [{ isPrimary: 'desc' }, { createdAt: 'asc' }],
|
||||
include: { partner: { select: { id: true, companyName: true } } },
|
||||
});
|
||||
|
||||
type TreeNode = (typeof accounts)[number] & { children: TreeNode[] };
|
||||
@@ -122,7 +332,7 @@ export class AdminPartnersService {
|
||||
const parent = nodeMap.get(account.parentAccountId.toString());
|
||||
if (parent) parent.children.push(node);
|
||||
else roots.push(node);
|
||||
} else {
|
||||
} else if (account.isPrimary === 1) {
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
@@ -134,8 +344,9 @@ export class AdminPartnersService {
|
||||
status: node.status,
|
||||
isPrimary: node.isPrimary,
|
||||
staffRole: node.staffRole,
|
||||
permissions: node.permissions,
|
||||
parentAccountId: node.parentAccountId,
|
||||
partner: node.partner,
|
||||
companyName: node.companyName,
|
||||
createdAt: node.createdAt,
|
||||
lastLoginAt: node.lastLoginAt,
|
||||
children: node.children.length ? node.children.map(mapNode) : undefined,
|
||||
@@ -148,27 +359,21 @@ export class AdminPartnersService {
|
||||
const account = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
partner: {
|
||||
select: {
|
||||
id: true,
|
||||
companyName: true,
|
||||
contactPhone: true,
|
||||
address: true,
|
||||
},
|
||||
},
|
||||
parent: { select: { id: true, name: true, phone: true } },
|
||||
parent: { select: { id: true, name: true, phone: true, companyName: true } },
|
||||
},
|
||||
});
|
||||
if (!account) throw new NotFoundException('开城合伙人账号不存在');
|
||||
if (!account) throw new NotFoundException('合伙人账号不存在');
|
||||
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(id);
|
||||
const orderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||
const [bills, orders] = await Promise.all([
|
||||
this.prisma.partnerBill.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
where: { partnerAccountId: primary.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
}),
|
||||
this.prisma.order.findMany({
|
||||
where: { city: { partnerId: account.partnerId } },
|
||||
where: orderWhere,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
select: {
|
||||
@@ -182,10 +387,14 @@ export class AdminPartnersService {
|
||||
}),
|
||||
]);
|
||||
|
||||
return serializeBigInt({ ...account, bills, orders });
|
||||
return serializeBigInt({ ...account, primaryAccountId: primary.id, bills, orders });
|
||||
}
|
||||
|
||||
async createPartnerAccount(dto: CreatePartnerAccountDto) {
|
||||
if (!dto.parentAccountId) {
|
||||
throw new BadRequestException('请指定主账号 parentAccountId 创建子账号');
|
||||
}
|
||||
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw new BadRequestException('请输入正确的手机号码');
|
||||
@@ -193,53 +402,40 @@ export class AdminPartnersService {
|
||||
const phoneTaken = await this.prisma.partnerAccount.findUnique({ where: { phone } });
|
||||
if (phoneTaken) throw new BadRequestException('该手机号已被使用');
|
||||
|
||||
if (dto.parentAccountId) {
|
||||
const parent = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: BigInt(dto.parentAccountId) },
|
||||
});
|
||||
if (!parent) throw new BadRequestException('主账号不存在');
|
||||
if (parent.isPrimary !== 1) throw new BadRequestException('仅可向主账号添加子账号');
|
||||
if (dto.partnerId && dto.partnerId !== parent.partnerId.toString()) {
|
||||
throw new BadRequestException('开城合伙人与主账号不匹配');
|
||||
}
|
||||
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: parent.partnerId,
|
||||
phone,
|
||||
name: dto.name.trim(),
|
||||
staffRole: (dto.staffRole ?? 'INTERNAL') as 'PARTNER' | 'INTERNAL' | 'PROMOTER',
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
include: { partner: { select: { id: true, companyName: true } } },
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
const parent = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: BigInt(dto.parentAccountId) },
|
||||
});
|
||||
if (!parent) throw new BadRequestException('主账号不存在');
|
||||
if (parent.isPrimary !== 1) {
|
||||
throw new BadRequestException('仅可向主账号添加子账号,不支持多级子账号');
|
||||
}
|
||||
|
||||
const partner = await this.prisma.partner.findUnique({ where: { id: BigInt(dto.partnerId!) } });
|
||||
if (!partner) throw new BadRequestException('开城合伙人不存在');
|
||||
const account = await this.prisma.partnerAccount.create({
|
||||
data: {
|
||||
partnerId: partner.id,
|
||||
phone,
|
||||
name: dto.name.trim(),
|
||||
staffRole: dto.staffRole ? (dto.staffRole as 'PARTNER' | 'INTERNAL' | 'PROMOTER') : undefined,
|
||||
staffRole: (dto.staffRole ?? 'INTERNAL') as 'PARTNER' | 'INTERNAL' | 'PROMOTER',
|
||||
permissions: dto.permissions ?? undefined,
|
||||
isPrimary: 0,
|
||||
parentAccountId: parent.id,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
include: { partner: { select: { id: true, companyName: true } } },
|
||||
});
|
||||
return serializeBigInt(account);
|
||||
}
|
||||
|
||||
async updatePartnerAccount(id: bigint, dto: UpdatePartnerAccountDto) {
|
||||
const existing = await this.prisma.partnerAccount.findUnique({ where: { id } });
|
||||
if (!existing) throw new NotFoundException('开城合伙人账号不存在');
|
||||
if (!existing) throw new NotFoundException('合伙人账号不存在');
|
||||
if (existing.isPrimary === 1) {
|
||||
throw new BadRequestException('请通过开城合伙人接口编辑主账号');
|
||||
}
|
||||
|
||||
const data: Prisma.PartnerAccountUpdateInput = {};
|
||||
if (dto.name !== undefined) data.name = dto.name;
|
||||
if (dto.status !== undefined) data.status = dto.status as 'ACTIVE' | 'DISABLED';
|
||||
if (dto.staffRole !== undefined) data.staffRole = dto.staffRole as 'PARTNER' | 'INTERNAL' | 'PROMOTER';
|
||||
if (dto.permissions !== undefined) data.permissions = dto.permissions;
|
||||
if (dto.phone !== undefined) {
|
||||
const phone = dto.phone.trim();
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
@@ -258,7 +454,7 @@ export class AdminPartnersService {
|
||||
|
||||
async deletePartnerSubAccount(id: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUnique({ where: { id } });
|
||||
if (!account) throw new NotFoundException('开城合伙人账号不存在');
|
||||
if (!account) throw new NotFoundException('合伙人账号不存在');
|
||||
if (!account.parentAccountId) {
|
||||
throw new BadRequestException('仅可删除子账号');
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class AdminRedeemService {
|
||||
where: { id },
|
||||
include: {
|
||||
user: true,
|
||||
store: { include: { partner: { select: { id: true, companyName: true } } } },
|
||||
store: { include: { partnerAccount: { select: { id: true, companyName: true } } } },
|
||||
coupon: true,
|
||||
payout: true,
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { mapStoreCompat } from '../../common/compat/v31-compat';
|
||||
import type { AdminStoreAccountsQueryDto, AdminStoreMediaQueryDto, AdminStoresQueryDto } from './dto/admin-query.dto';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import type {
|
||||
CreateStoreAccountDto,
|
||||
CreateStoreDto,
|
||||
@@ -16,7 +17,10 @@ import type {
|
||||
|
||||
@Injectable()
|
||||
export class AdminStoresService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async listStores(query: AdminStoresQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
@@ -25,7 +29,7 @@ export class AdminStoresService {
|
||||
if (query.name) where.name = { contains: query.name };
|
||||
if (query.status) where.status = query.status as Prisma.EnumStoreStatusFilter['equals'];
|
||||
if (query.cityId) where.cityId = BigInt(query.cityId);
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.partnerId) where.partnerAccountId = BigInt(query.partnerId);
|
||||
if (query.phone) where.phone = { contains: query.phone };
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
@@ -36,7 +40,7 @@ export class AdminStoresService {
|
||||
take: pageSize,
|
||||
include: {
|
||||
cityRef: { select: { id: true, name: true, code: true } },
|
||||
partner: { select: { id: true, companyName: true } },
|
||||
partnerAccount: { select: { id: true, companyName: true } },
|
||||
account: { select: { id: true, phone: true, name: true, status: true } },
|
||||
coverResource: { select: { id: true, url: true } },
|
||||
},
|
||||
@@ -56,7 +60,7 @@ export class AdminStoresService {
|
||||
where: { id },
|
||||
include: {
|
||||
cityRef: true,
|
||||
partner: true,
|
||||
partnerAccount: true,
|
||||
category: true,
|
||||
account: true,
|
||||
coverResource: true,
|
||||
@@ -123,6 +127,7 @@ export class AdminStoresService {
|
||||
...(dto.intro !== undefined ? { intro: dto.intro } : {}),
|
||||
...(dto.address !== undefined ? { address: dto.address } : {}),
|
||||
...(dto.district !== undefined ? { district: dto.district } : {}),
|
||||
...(dto.settlementRate !== undefined ? { settlementRate: dto.settlementRate } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -162,18 +167,22 @@ export class AdminStoresService {
|
||||
});
|
||||
if (existingAccount) throw new BadRequestException('该手机号已绑定门店');
|
||||
|
||||
const partner = await this.prisma.partner.findUnique({ where: { id: BigInt(dto.partnerId) } });
|
||||
if (!partner) throw new BadRequestException('开城合伙人不存在');
|
||||
const partnerAccountId = BigInt(dto.partnerAccountId);
|
||||
const partnerAccount = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
if (!partnerAccount || partnerAccount.isPrimary !== 1) {
|
||||
throw new BadRequestException('开城合伙人不存在');
|
||||
}
|
||||
const city = await this.prisma.commonCity.findUnique({ where: { id: BigInt(dto.cityId) } });
|
||||
if (!city) throw new BadRequestException('开城城市不存在');
|
||||
if (city.partnerId && city.partnerId !== partner.id) {
|
||||
throw new BadRequestException('开城城市与合伙人不匹配');
|
||||
}
|
||||
await this.partnerCityService.assertPartnerAccountBoundToCity(partnerAccountId, city.id);
|
||||
|
||||
const store = await this.prisma.store.create({
|
||||
data: {
|
||||
cityId: city.id,
|
||||
partnerId: partner.id,
|
||||
partnerAccountId,
|
||||
settlementRate: dto.settlementRate ?? 0.6,
|
||||
categoryId: dto.categoryId ? BigInt(dto.categoryId) : null,
|
||||
name: dto.name,
|
||||
phone: normalizedPhone,
|
||||
@@ -359,7 +368,7 @@ export class AdminStoresService {
|
||||
async detailStoreAccount(id: bigint) {
|
||||
const account = await this.prisma.storeAccount.findUnique({
|
||||
where: { id },
|
||||
include: { store: { include: { cityRef: true, partner: true } } },
|
||||
include: { store: { include: { cityRef: true, partnerAccount: true } } },
|
||||
});
|
||||
if (!account) throw new NotFoundException('门店账号不存在');
|
||||
return serializeBigInt(account);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BenefitService } from '../benefit/benefit.service';
|
||||
import { TradeService } from '../trade/trade.service';
|
||||
import { TicketService } from '../common/ticket.service';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import type { TicketListQueryDto } from '../common/dto/common-query.dto';
|
||||
|
||||
@Injectable()
|
||||
@@ -13,6 +14,7 @@ export class AdminTicketsService {
|
||||
private readonly ticketService: TicketService,
|
||||
private readonly tradeService: TradeService,
|
||||
private readonly benefitService: BenefitService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
list(query: TicketListQueryDto) {
|
||||
@@ -82,15 +84,9 @@ export class AdminTicketsService {
|
||||
}
|
||||
|
||||
async listPartnerReshipments(partnerAccountId: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
const cities = await this.prisma.commonCity.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
select: { id: true },
|
||||
});
|
||||
const orderWhere = await this.partnerCityService.buildPartnerOrderWhere(partnerAccountId);
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: { cityId: { in: cities.map((c) => c.id) } },
|
||||
where: orderWhere,
|
||||
select: { id: true },
|
||||
});
|
||||
const orderIds = orders.map((o) => o.id);
|
||||
|
||||
@@ -213,11 +213,23 @@ export class AdminWechatBindingsService {
|
||||
|
||||
const accounts = await this.prisma.partnerAccount.findMany({
|
||||
where,
|
||||
include: { partner: { select: { id: true, companyName: true } } },
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
name: true,
|
||||
wxOpenId: true,
|
||||
wxUnionId: true,
|
||||
lastLoginAt: true,
|
||||
status: true,
|
||||
isPrimary: true,
|
||||
parentAccountId: true,
|
||||
companyName: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const a of accounts) {
|
||||
if (!a.wxOpenId) continue;
|
||||
const refId = a.isPrimary === 1 ? a.id : (a.parentAccountId ?? a.id);
|
||||
rows.push({
|
||||
actorType: 'PARTNER',
|
||||
actorId: a.id,
|
||||
@@ -225,8 +237,8 @@ export class AdminWechatBindingsService {
|
||||
name: a.name,
|
||||
wxOpenId: a.wxOpenId,
|
||||
wxUnionId: a.wxUnionId,
|
||||
refId: a.partnerId,
|
||||
refLabel: a.partner.companyName,
|
||||
refId,
|
||||
refLabel: a.companyName,
|
||||
lastLoginAt: a.lastLoginAt,
|
||||
status: a.status,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
Max,
|
||||
MinLength,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
@@ -22,7 +23,7 @@ export class UpdateStoreStatusDto {
|
||||
export class CreateStoreDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
partnerId: string;
|
||||
partnerAccountId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@@ -88,6 +89,11 @@ export class CreateStoreDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contractUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
settlementRate?: number;
|
||||
}
|
||||
|
||||
export class UpdateStoreDto {
|
||||
@@ -114,6 +120,11 @@ export class UpdateStoreDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
district?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
settlementRate?: number;
|
||||
}
|
||||
|
||||
export class CreateStoreAccountDto {
|
||||
@@ -145,6 +156,18 @@ export class UpdateStoreAccountDto {
|
||||
}
|
||||
|
||||
export class CreatePartnerDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
cityId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
companyName: string;
|
||||
@@ -153,9 +176,40 @@ export class CreatePartnerDto {
|
||||
@IsNotEmpty()
|
||||
address: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contactPhone: string;
|
||||
contactPhone?: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['CITY_WIDE', 'DISTRICT'])
|
||||
scopeType: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
districtCodes?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
orderCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
redeemCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
bindingStatus?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
managedWarehouseId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contractNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@@ -168,9 +222,21 @@ export class CreatePartnerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankBranch?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
weeklyStoreTarget?: number;
|
||||
}
|
||||
|
||||
export class UpdatePartnerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
companyName?: string;
|
||||
@@ -183,6 +249,38 @@ export class UpdatePartnerDto {
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['CITY_WIDE', 'DISTRICT'])
|
||||
scopeType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
districtCodes?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
orderCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
redeemCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
bindingStatus?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== null)
|
||||
@IsString()
|
||||
managedWarehouseId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contractNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankAccountName?: string;
|
||||
@@ -194,6 +292,14 @@ export class UpdatePartnerDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bankBranch?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
weeklyStoreTarget?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class UpdatePartnerAccountDto {
|
||||
@@ -208,13 +314,21 @@ export class UpdatePartnerAccountDto {
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'DISABLED'])
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['PARTNER', 'INTERNAL', 'PROMOTER'])
|
||||
staffRole?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class CreatePartnerAccountDto {
|
||||
@ValidateIf((o: CreatePartnerAccountDto) => !o.parentAccountId)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
partnerId?: string;
|
||||
parentAccountId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@@ -228,10 +342,10 @@ export class CreatePartnerAccountDto {
|
||||
@IsIn(['PARTNER', 'INTERNAL', 'PROMOTER'])
|
||||
staffRole?: string;
|
||||
|
||||
/** 主账号 ID;传入则创建子账号 */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentAccountId?: string;
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class CreateCityDto {
|
||||
@@ -247,10 +361,6 @@ export class CreateCityDto {
|
||||
@IsNotEmpty()
|
||||
province: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['PENDING', 'ACTIVE', 'PAUSED'])
|
||||
status?: string;
|
||||
@@ -265,10 +375,6 @@ export class UpdateCityDto {
|
||||
@IsString()
|
||||
province?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['PENDING', 'ACTIVE', 'PAUSED'])
|
||||
status?: string;
|
||||
@@ -278,6 +384,127 @@ export class UpdateCityDto {
|
||||
|
||||
@IsOptional()
|
||||
crossMinQty?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
maxPartnerCommissionRate?: number;
|
||||
}
|
||||
|
||||
export class BindCityPartnerDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
partnerId: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['CITY_WIDE', 'DISTRICT'])
|
||||
scopeType: 'CITY_WIDE' | 'DISTRICT';
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
districtCodes?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
orderCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
redeemCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
status?: 'ACTIVE' | 'PAUSED';
|
||||
}
|
||||
|
||||
export class UpdateCityPartnerDto {
|
||||
@IsOptional()
|
||||
@IsIn(['CITY_WIDE', 'DISTRICT'])
|
||||
scopeType?: 'CITY_WIDE' | 'DISTRICT';
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
districtCodes?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
orderCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
redeemCommissionRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
status?: 'ACTIVE' | 'PAUSED';
|
||||
}
|
||||
|
||||
export class CreateCityWarehouseDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
address: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contactName: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contactPhone: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['HQ', 'PARTNER'])
|
||||
managerType: 'HQ' | 'PARTNER';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerAccountId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
status?: 'ACTIVE' | 'PAUSED';
|
||||
}
|
||||
|
||||
export class UpdateCityWarehouseDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['HQ', 'PARTNER'])
|
||||
managerType?: 'HQ' | 'PARTNER';
|
||||
|
||||
@IsOptional()
|
||||
@ValidateIf((_, v) => v !== null)
|
||||
@IsString()
|
||||
partnerAccountId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['ACTIVE', 'PAUSED'])
|
||||
status?: 'ACTIVE' | 'PAUSED';
|
||||
}
|
||||
|
||||
export class CreateStoreMediaDto {
|
||||
|
||||
@@ -109,6 +109,36 @@ export class AdminPartnersQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contactPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
partnerId?: string;
|
||||
}
|
||||
|
||||
export class AdminCityWarehousesQueryDto extends PaginationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
managerType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export class AdminPartnerAccountsQueryDto extends PaginationQueryDto {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CityScopeModule } from '../city-scope/city-scope.module';
|
||||
import { IamModule } from '../iam/iam.module';
|
||||
import { TradeModule } from '../trade/trade.module';
|
||||
import { AdminDashboardController } from './admin-dashboard.controller';
|
||||
@@ -12,6 +13,7 @@ import { AdminStoresService } from './admin-stores.service';
|
||||
import { AdminPartnersController, AdminPartnerAccountsController } from './admin-partners.controller';
|
||||
import { AdminPartnersService } from './admin-partners.service';
|
||||
import { AdminCitiesController } from './admin-cities.controller';
|
||||
import { AdminCityWarehousesController, AdminCityWarehouseMutationsController } from './admin-city-warehouses.controller';
|
||||
import { AdminCitiesService } from './admin-cities.service';
|
||||
import { AdminBenefitCouponsController, AdminBenefitLedgersController } from './admin-benefit.controller';
|
||||
import { AdminBenefitService } from './admin-benefit.service';
|
||||
@@ -50,7 +52,7 @@ import { AdminHqPermissionsController } from './admin-hq-permissions.controller'
|
||||
import { AdminHqPermissionsService } from './admin-hq-permissions.service';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
|
||||
imports: [CityScopeModule, IamModule, TradeModule, BenefitModule, CommonModule, IntegrationsModule, RedeemModule],
|
||||
controllers: [
|
||||
AdminDashboardController,
|
||||
AdminUsersController,
|
||||
@@ -61,6 +63,8 @@ import { AdminHqPermissionsService } from './admin-hq-permissions.service';
|
||||
AdminPartnersController,
|
||||
AdminPartnerAccountsController,
|
||||
AdminCitiesController,
|
||||
AdminCityWarehousesController,
|
||||
AdminCityWarehouseMutationsController,
|
||||
AdminBenefitCouponsController,
|
||||
AdminBenefitLedgersController,
|
||||
AdminRedeemRecordsController,
|
||||
@@ -104,5 +108,6 @@ import { AdminHqPermissionsService } from './admin-hq-permissions.service';
|
||||
AdminHqPermissionsService,
|
||||
SuperAdminGuard,
|
||||
],
|
||||
exports: [CityScopeModule],
|
||||
})
|
||||
export class OpsModule {}
|
||||
|
||||
@@ -236,10 +236,7 @@ export class RedeemService {
|
||||
}
|
||||
|
||||
const amount = tokenAmount;
|
||||
const cityRule = await this.prisma.commonCityCommissionRule.findUnique({
|
||||
where: { cityId: account.store.cityId },
|
||||
});
|
||||
const settlementRate = cityRule ? Number(cityRule.storeSettlementRate) : 0.6;
|
||||
const settlementRate = Number(account.store.settlementRate);
|
||||
const settleAmount = calcRedeemSettleAmount(amount, settlementRate);
|
||||
|
||||
let record;
|
||||
|
||||
@@ -152,15 +152,22 @@ export class PartnerMeController {
|
||||
async me(@CurrentUser() user: AuthUser) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: user.actorId },
|
||||
include: { partner: true },
|
||||
});
|
||||
let primary = account;
|
||||
if (account.isPrimary !== 1 && account.parentAccountId) {
|
||||
primary = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: account.parentAccountId },
|
||||
});
|
||||
}
|
||||
return {
|
||||
id: account.id.toString(),
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
staffRole: account.staffRole ?? undefined,
|
||||
companyName: account.partner.companyName,
|
||||
permissions: Array.isArray(account.permissions) ? account.permissions : undefined,
|
||||
primaryAccountId: primary.id.toString(),
|
||||
companyName: primary.companyName ?? undefined,
|
||||
hasWechat: !!account.wxOpenId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { IamModule } from '../iam/iam.module';
|
||||
import { AnalyticsModule } from '../analytics/analytics.module';
|
||||
import { CityScopeModule } from '../city-scope/city-scope.module';
|
||||
import { SettlementService } from './settlement.service';
|
||||
import {
|
||||
AdminPartnerBillController,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
} from './settlement.controller';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule, AnalyticsModule],
|
||||
imports: [IamModule, AnalyticsModule, CityScopeModule],
|
||||
controllers: [
|
||||
SettlementController,
|
||||
PartnerMeController,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
|
||||
function generateBillNo() {
|
||||
return `PB${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
|
||||
@@ -13,6 +14,7 @@ export class SettlementService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async createStorePayout(
|
||||
@@ -152,16 +154,13 @@ export class SettlementService {
|
||||
return results;
|
||||
}
|
||||
|
||||
async listPartnerBills(partnerAccountId: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
async listPartnerBills(partnerAccountId: bigint) { const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const bills = await this.prisma.partnerBill.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
where: { partnerAccountId: primary.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||
partnerId: account.partnerId,
|
||||
partnerAccountId: primary.id,
|
||||
eventName: 'partner_bill_view',
|
||||
extraJson: { count: bills.length },
|
||||
});
|
||||
@@ -178,7 +177,7 @@ export class SettlementService {
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
const where: Prisma.PartnerBillWhereInput = {};
|
||||
if (query.status) where.status = query.status as Prisma.EnumPartnerBillStatusFilter['equals'];
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.partnerId) where.partnerAccountId = BigInt(query.partnerId);
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.partnerBill.findMany({
|
||||
@@ -186,7 +185,7 @@ export class SettlementService {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: { partner: { select: { companyName: true } } },
|
||||
include: { partnerAccount: { select: { companyName: true } } },
|
||||
}),
|
||||
this.prisma.partnerBill.count({ where }),
|
||||
]);
|
||||
@@ -196,20 +195,21 @@ export class SettlementService {
|
||||
async getAdminPartnerBill(id: bigint) {
|
||||
const bill = await this.prisma.partnerBill.findUnique({
|
||||
where: { id },
|
||||
include: { partner: true },
|
||||
include: { partnerAccount: true },
|
||||
});
|
||||
if (!bill) throw new NotFoundException('账单不存在');
|
||||
return serializeBigInt(bill);
|
||||
}
|
||||
|
||||
async generatePartnerBill(body: { partnerId: string; year: number; month: number }) {
|
||||
const partnerId = BigInt(body.partnerId);
|
||||
const partnerAccountId = BigInt(body.partnerId);
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const periodStart = new Date(body.year, body.month - 1, 1);
|
||||
const periodEnd = new Date(body.year, body.month, 0, 23, 59, 59, 999);
|
||||
|
||||
const existing = await this.prisma.partnerBill.findFirst({
|
||||
where: {
|
||||
partnerId,
|
||||
partnerAccountId: primary.id,
|
||||
periodStart,
|
||||
status: { not: 'DRAFT' },
|
||||
},
|
||||
@@ -218,31 +218,33 @@ export class SettlementService {
|
||||
throw new BadRequestException('该月账单已确认,不可重复生成');
|
||||
}
|
||||
|
||||
const cities = await this.prisma.commonCity.findMany({
|
||||
where: { partnerId },
|
||||
include: { commissionRule: true },
|
||||
});
|
||||
const cityIds = cities.map((c) => c.id);
|
||||
const defaultOrderRate = cities[0]?.commissionRule?.orderCommissionRate
|
||||
? Number(cities[0].commissionRule.orderCommissionRate)
|
||||
: 0;
|
||||
const defaultRedeemRate = cities[0]?.commissionRule?.redeemCommissionRate
|
||||
? Number(cities[0].commissionRule.redeemCommissionRate)
|
||||
: 0.03;
|
||||
if (!primary.cityId) {
|
||||
throw new BadRequestException('合伙人未绑定开城城市');
|
||||
}
|
||||
|
||||
const orderCommissionRate = Number(primary.orderCommissionRate ?? 0);
|
||||
const redeemCommissionRate = Number(primary.redeemCommissionRate ?? 0.03);
|
||||
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: {
|
||||
cityId: { in: cityIds },
|
||||
cityId: primary.cityId,
|
||||
payStatus: 'PAID',
|
||||
paidAt: { gte: periodStart, lte: periodEnd },
|
||||
},
|
||||
});
|
||||
const orderCommission = orders.reduce(
|
||||
(sum, o) => sum + Number(o.payAmount) * defaultOrderRate,
|
||||
0,
|
||||
);
|
||||
const orderCommission = orders.reduce((sum, o) => {
|
||||
if (o.partnerAccountIdAtPay) {
|
||||
if (o.partnerAccountIdAtPay !== primary.id) return sum;
|
||||
const rate = o.orderCommissionRateAtPay != null ? Number(o.orderCommissionRateAtPay) : 0;
|
||||
return sum + Number(o.payAmount) * rate;
|
||||
}
|
||||
return sum + Number(o.payAmount) * orderCommissionRate;
|
||||
}, 0);
|
||||
|
||||
const stores = await this.prisma.store.findMany({ where: { partnerId }, select: { id: true } });
|
||||
const stores = await this.prisma.store.findMany({
|
||||
where: { partnerAccountId: primary.id },
|
||||
select: { id: true },
|
||||
});
|
||||
const storeIds = stores.map((s) => s.id);
|
||||
const redeems = await this.prisma.redeemRecord.findMany({
|
||||
where: {
|
||||
@@ -251,14 +253,14 @@ export class SettlementService {
|
||||
},
|
||||
});
|
||||
const redeemCommission = redeems.reduce(
|
||||
(sum, r) => sum + Number(r.amount) * defaultRedeemRate,
|
||||
(sum, r) => sum + Number(r.amount) * redeemCommissionRate,
|
||||
0,
|
||||
);
|
||||
|
||||
const totalAmount = Math.round((orderCommission + redeemCommission) * 100) / 100;
|
||||
|
||||
const draft = await this.prisma.partnerBill.findFirst({
|
||||
where: { partnerId, periodStart, status: 'DRAFT' },
|
||||
where: { partnerAccountId: primary.id, periodStart, status: 'DRAFT' },
|
||||
});
|
||||
|
||||
const bill = draft
|
||||
@@ -269,7 +271,7 @@ export class SettlementService {
|
||||
: await this.prisma.partnerBill.create({
|
||||
data: {
|
||||
billNo: generateBillNo(),
|
||||
partnerId,
|
||||
partnerAccountId: primary.id,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
orderCommission,
|
||||
@@ -310,12 +312,12 @@ export class SettlementService {
|
||||
|
||||
async exportPartnerBills(query: { partnerId?: string; status?: string }) {
|
||||
const where: Prisma.PartnerBillWhereInput = {};
|
||||
if (query.partnerId) where.partnerId = BigInt(query.partnerId);
|
||||
if (query.partnerId) where.partnerAccountId = BigInt(query.partnerId);
|
||||
if (query.status) where.status = query.status as Prisma.EnumPartnerBillStatusFilter['equals'];
|
||||
|
||||
const bills = await this.prisma.partnerBill.findMany({
|
||||
where,
|
||||
include: { partner: { select: { companyName: true } } },
|
||||
include: { partnerAccount: { select: { companyName: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
@@ -323,7 +325,7 @@ export class SettlementService {
|
||||
const rows = bills.map((b) =>
|
||||
[
|
||||
b.billNo,
|
||||
b.partner.companyName,
|
||||
b.partnerAccount.companyName,
|
||||
b.periodStart.toISOString().slice(0, 10),
|
||||
b.periodEnd.toISOString().slice(0, 10),
|
||||
Number(b.orderCommission),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Module, forwardRef } from '@nestjs/common';
|
||||
import { IamModule } from '../iam/iam.module';
|
||||
import { RedeemModule } from '../redeem/redeem.module';
|
||||
import { AnalyticsModule } from '../analytics/analytics.module';
|
||||
import { CityScopeModule } from '../city-scope/city-scope.module';
|
||||
import { StoreService } from './store.service';
|
||||
import {
|
||||
PartnerDashboardController,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
} from './store.controller';
|
||||
|
||||
@Module({
|
||||
imports: [IamModule, AnalyticsModule, forwardRef(() => RedeemModule)],
|
||||
imports: [IamModule, AnalyticsModule, CityScopeModule, forwardRef(() => RedeemModule)],
|
||||
controllers: [
|
||||
PublicStoreController,
|
||||
PartnerStoreController,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { serializeBigInt } from '../../common/decorators/current-user.decorator'
|
||||
import { mapStoreCompat } from '../../common/compat/v31-compat';
|
||||
import { parseBigIntParam } from '../../common/parse-bigint';
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
|
||||
@Injectable()
|
||||
export class StoreService {
|
||||
@@ -19,6 +20,7 @@ export class StoreService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async listOpenStores(cityCode?: string) {
|
||||
@@ -48,9 +50,15 @@ export class StoreService {
|
||||
return serializeBigInt(mapStoreCompat({ ...store, media }));
|
||||
}
|
||||
|
||||
private async resolvePartnerScope(actorAccountId: bigint) {
|
||||
const account = await this.getPartnerAccount(actorAccountId);
|
||||
const primaryId = await this.getPartnerPrimaryId(actorAccountId);
|
||||
return { account, primaryId };
|
||||
}
|
||||
|
||||
async partnerListStores(partnerAccountId: bigint) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const where: { partnerId: bigint; id?: { in: bigint[] } } = { partnerId: account.partnerId };
|
||||
const { account, primaryId } = await this.resolvePartnerScope(partnerAccountId);
|
||||
const where: { partnerAccountId: bigint; id?: { in: bigint[] } } = { partnerAccountId: primaryId };
|
||||
if (this.isSubAccount(account)) {
|
||||
const storeIds = await this.getStoreIdsCreatedByAccount(partnerAccountId);
|
||||
if (storeIds.length === 0) return [];
|
||||
@@ -65,9 +73,9 @@ export class StoreService {
|
||||
}
|
||||
|
||||
async partnerGetStore(partnerAccountId: bigint, storeId: bigint) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const { account, primaryId } = await this.resolvePartnerScope(partnerAccountId);
|
||||
const store = await this.prisma.store.findFirst({
|
||||
where: { id: storeId, partnerId: account.partnerId },
|
||||
where: { id: storeId, partnerAccountId: primaryId },
|
||||
include: { category: true, coverResource: true },
|
||||
});
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
@@ -88,10 +96,11 @@ export class StoreService {
|
||||
}
|
||||
|
||||
async partnerListCities(partnerAccountId: bigint) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const { primaryId } = await this.resolvePartnerScope(partnerAccountId);
|
||||
const cityWhere = await this.partnerCityService.buildPartnerCityWhere(primaryId);
|
||||
const cities = await this.prisma.commonCity.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
select: { id: true, name: true, code: true, province: true, partnerId: true },
|
||||
where: cityWhere,
|
||||
select: { id: true, name: true, code: true, province: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return serializeBigInt(cities);
|
||||
@@ -115,11 +124,11 @@ export class StoreService {
|
||||
}
|
||||
|
||||
async createStore(partnerAccountId: bigint, body: Record<string, unknown>) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const { primaryId } = await this.resolvePartnerScope(partnerAccountId);
|
||||
const normalizedPhone = String(body.phone).trim();
|
||||
await this.assertStorePhoneAvailable(normalizedPhone);
|
||||
|
||||
const city = await this.resolvePartnerCity(account.partnerId, body.cityId);
|
||||
const city = await this.resolvePartnerCity(partnerAccountId, body.cityId);
|
||||
const coverUrl = body.coverUrl ? String(body.coverUrl).trim() : '';
|
||||
const envPhotoUrls = Array.isArray(body.envPhotoUrls)
|
||||
? body.envPhotoUrls.map((u) => String(u).trim()).filter(Boolean)
|
||||
@@ -133,7 +142,7 @@ export class StoreService {
|
||||
const store = await this.prisma.store.create({
|
||||
data: {
|
||||
cityId: city.id,
|
||||
partnerId: account.partnerId,
|
||||
partnerAccountId: primaryId,
|
||||
categoryId: body.categoryId ? parseBigIntParam(body.categoryId, '分类ID') : null,
|
||||
name: String(body.name),
|
||||
phone: normalizedPhone,
|
||||
@@ -220,7 +229,7 @@ export class StoreService {
|
||||
});
|
||||
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||
partnerId: account.partnerId,
|
||||
partnerAccountId: primaryId,
|
||||
eventName: 'partner_store_create',
|
||||
refType: 'STORE',
|
||||
refId: store.id,
|
||||
@@ -240,10 +249,10 @@ export class StoreService {
|
||||
storeId: bigint,
|
||||
status: 'OPEN' | 'PAUSED' | 'CLOSED',
|
||||
) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const { account, primaryId } = await this.resolvePartnerScope(partnerAccountId);
|
||||
this.assertPrimaryAccount(account);
|
||||
const store = await this.prisma.store.findFirst({
|
||||
where: { id: storeId, partnerId: account.partnerId },
|
||||
where: { id: storeId, partnerAccountId: primaryId },
|
||||
});
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
if (store.status === 'CLOSED') {
|
||||
@@ -259,7 +268,7 @@ export class StoreService {
|
||||
include: { coverResource: true },
|
||||
});
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||
partnerId: account.partnerId,
|
||||
partnerAccountId: primaryId,
|
||||
eventName: 'partner_store_status_change',
|
||||
refType: 'STORE',
|
||||
refId: storeId,
|
||||
@@ -276,10 +285,10 @@ export class StoreService {
|
||||
storeId: bigint,
|
||||
body: Record<string, unknown>,
|
||||
) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const { account, primaryId } = await this.resolvePartnerScope(partnerAccountId);
|
||||
this.assertPrimaryAccount(account);
|
||||
const store = await this.prisma.store.findFirst({
|
||||
where: { id: storeId, partnerId: account.partnerId },
|
||||
where: { id: storeId, partnerAccountId: primaryId },
|
||||
});
|
||||
if (!store) throw new NotFoundException('门店不存在');
|
||||
if (store.status === 'CLOSED') {
|
||||
@@ -343,20 +352,20 @@ export class StoreService {
|
||||
}
|
||||
|
||||
async partnerDashboard(partnerAccountId: bigint) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
const { account, primaryId } = await this.resolvePartnerScope(partnerAccountId);
|
||||
this.assertPrimaryAccount(account);
|
||||
const partnerStoreIds = await this.prisma.store.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
where: { partnerAccountId: primaryId },
|
||||
select: { id: true },
|
||||
});
|
||||
const storeIds = partnerStoreIds.map((s) => s.id);
|
||||
const [storeCount, orderCount, recentStores, pendingAuditCount] = await Promise.all([
|
||||
this.prisma.store.count({ where: { partnerId: account.partnerId } }),
|
||||
this.prisma.store.count({ where: { partnerAccountId: primaryId } }),
|
||||
this.prisma.order.count({
|
||||
where: { city: { partnerId: account.partnerId } },
|
||||
where: await this.partnerCityService.buildPartnerOrderWhere(primaryId),
|
||||
}),
|
||||
this.prisma.store.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
where: { partnerAccountId: primaryId },
|
||||
select: { id: true, name: true, status: true, createdAt: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
@@ -375,18 +384,17 @@ export class StoreService {
|
||||
return {
|
||||
storeCount,
|
||||
orderCount,
|
||||
companyName: account.partner.companyName,
|
||||
companyName: account.companyName ?? '',
|
||||
recentStores: serializeBigInt(recentStores),
|
||||
pendingAuditCount,
|
||||
};
|
||||
}
|
||||
|
||||
async partnerLeaderboard(partnerAccountId: bigint, period: PartnerLeaderboardPeriod = 'total') {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
this.assertPrimaryAccount(account);
|
||||
const { primaryId } = await this.resolvePartnerScope(partnerAccountId);
|
||||
|
||||
const accounts = await this.prisma.partnerAccount.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
where: { OR: [{ id: primaryId }, { parentAccountId: primaryId }] },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
|
||||
@@ -457,10 +465,9 @@ export class StoreService {
|
||||
return { period, list, self };
|
||||
}
|
||||
|
||||
async partnerWeeklyReport(partnerAccountId: bigint, startDate?: string) {
|
||||
const account = await this.getPartnerAccount(partnerAccountId);
|
||||
async partnerWeeklyReport(actorAccountId: bigint, startDate?: string) {
|
||||
const { account, primaryId } = await this.resolvePartnerScope(actorAccountId);
|
||||
this.assertPrimaryAccount(account);
|
||||
const partnerId = account.partnerId;
|
||||
|
||||
const currentWeekStart = this.startOfWeekMonday(new Date());
|
||||
const periodStart =
|
||||
@@ -472,16 +479,16 @@ export class StoreService {
|
||||
const prevPeriodEnd = periodStart;
|
||||
|
||||
const newStoreTarget =
|
||||
account.partner.weeklyStoreTarget ??
|
||||
account.weeklyStoreTarget ??
|
||||
Number(process.env.PARTNER_WEEKLY_STORE_TARGET ?? 20);
|
||||
|
||||
const orderWhere = {
|
||||
city: { partnerId },
|
||||
...(await this.partnerCityService.buildPartnerOrderWhere(primaryId)),
|
||||
payStatus: 'PAID' as const,
|
||||
paidAt: { gte: periodStart, lt: periodEnd },
|
||||
};
|
||||
const prevOrderWhere = {
|
||||
city: { partnerId },
|
||||
...(await this.partnerCityService.buildPartnerOrderWhere(primaryId)),
|
||||
payStatus: 'PAID' as const,
|
||||
paidAt: { gte: prevPeriodStart, lt: prevPeriodEnd },
|
||||
};
|
||||
@@ -498,16 +505,16 @@ export class StoreService {
|
||||
] = await Promise.all([
|
||||
this.prisma.order.aggregate({ where: orderWhere, _sum: { payAmount: true } }),
|
||||
this.prisma.order.count({ where: orderWhere }),
|
||||
this.prisma.store.count({ where: { partnerId } }),
|
||||
this.prisma.store.count({ where: { partnerAccountId: primaryId } }),
|
||||
this.prisma.store.count({
|
||||
where: { partnerId, createdAt: { gte: periodStart, lt: periodEnd } },
|
||||
where: { partnerAccountId: primaryId, createdAt: { gte: periodStart, lt: periodEnd } },
|
||||
}),
|
||||
this.prisma.order.aggregate({ where: prevOrderWhere, _sum: { payAmount: true } }),
|
||||
this.prisma.redeemRecord.groupBy({
|
||||
by: ['storeId'],
|
||||
where: {
|
||||
createdAt: { gte: periodStart, lt: periodEnd },
|
||||
store: { partnerId },
|
||||
store: { partnerAccountId: primaryId },
|
||||
},
|
||||
_sum: { amount: true },
|
||||
orderBy: { _sum: { amount: 'desc' } },
|
||||
@@ -516,7 +523,7 @@ export class StoreService {
|
||||
this.prisma.redeemRecord.findMany({
|
||||
where: {
|
||||
createdAt: { gte: periodStart, lt: periodEnd },
|
||||
store: { partnerId },
|
||||
store: { partnerAccountId: primaryId },
|
||||
},
|
||||
select: { storeId: true },
|
||||
distinct: ['storeId'],
|
||||
@@ -677,22 +684,29 @@ export class StoreService {
|
||||
return ['周一', '周二', '周三', '周四', '周五', '周六', '周日'][index] ?? '';
|
||||
}
|
||||
|
||||
private async getPartnerPrimaryId(actorAccountId: bigint) {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(actorAccountId);
|
||||
return primary.id;
|
||||
}
|
||||
|
||||
private async getPartnerAccount(partnerAccountId: bigint) {
|
||||
return this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: partnerAccountId },
|
||||
include: { partner: true },
|
||||
});
|
||||
}
|
||||
|
||||
private async resolvePartnerCity(partnerId: bigint, cityId: unknown) {
|
||||
private async resolvePartnerCity(actorAccountId: bigint, cityId: unknown) {
|
||||
const primaryId = await this.getPartnerPrimaryId(actorAccountId);
|
||||
if (cityId) {
|
||||
const city = await this.prisma.commonCity.findFirst({
|
||||
where: { id: parseBigIntParam(cityId, '城市ID'), partnerId },
|
||||
});
|
||||
const id = parseBigIntParam(cityId, '城市ID');
|
||||
await this.partnerCityService.assertPartnerAccountBoundToCity(primaryId, id);
|
||||
const city = await this.prisma.commonCity.findUnique({ where: { id } });
|
||||
if (!city) throw new BadRequestException('所选地区未匹配到开城城市');
|
||||
return city;
|
||||
}
|
||||
const city = await this.prisma.commonCity.findFirst({ where: { partnerId } });
|
||||
const cityIds = await this.partnerCityService.listCityIdsForPartnerAccount(primaryId);
|
||||
if (!cityIds.length) throw new BadRequestException('合伙人未绑定开城');
|
||||
const city = await this.prisma.commonCity.findFirst({ where: { id: { in: cityIds } } });
|
||||
if (!city) throw new BadRequestException('合伙人未绑定开城');
|
||||
return city;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@ import { IamModule } from '../iam/iam.module';
|
||||
import { BenefitModule } from '../benefit/benefit.module';
|
||||
import { CatalogModule } from '../catalog/catalog.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { CityScopeModule } from '../city-scope/city-scope.module';
|
||||
import { TradeController, PartnerOrderController, PartnerReshipmentController } from './trade.controller';
|
||||
import { TradeService } from './trade.service';
|
||||
|
||||
@Module({
|
||||
imports: [IntegrationsModule, IamModule, CatalogModule, AnalyticsModule, forwardRef(() => BenefitModule), CommonModule],
|
||||
imports: [IntegrationsModule, IamModule, CatalogModule, AnalyticsModule, CityScopeModule, forwardRef(() => BenefitModule), CommonModule],
|
||||
controllers: [TradeController, PartnerOrderController, PartnerReshipmentController],
|
||||
providers: [TradeService],
|
||||
exports: [TradeService],
|
||||
|
||||
@@ -17,6 +17,7 @@ import { serializeBigInt } from '../../common/decorators/current-user.decorator'
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { CatalogService } from '../catalog/catalog.service';
|
||||
import { BenefitService } from '../benefit/benefit.service';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import { TicketService } from '../common/ticket.service';
|
||||
import { PAY_PROVIDER, DELIVERY_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import { IPayProvider } from '../../integrations/pay/pay.interface';
|
||||
@@ -39,6 +40,7 @@ export class TradeService {
|
||||
@Inject(PAY_PROVIDER) private readonly payProvider: IPayProvider,
|
||||
@Inject(DELIVERY_PROVIDER) private readonly deliveryProvider: IDeliveryProvider,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
) {}
|
||||
|
||||
async preview(userId: bigint, body: { productId: string; quantity: number; addressId?: string }) {
|
||||
@@ -220,6 +222,10 @@ export class TradeService {
|
||||
|
||||
const { externalNo } = payResult;
|
||||
const now = new Date();
|
||||
const paySnapshot = await this.partnerCityService.resolveForOrder(
|
||||
order.cityId,
|
||||
order.receiverDistrict,
|
||||
);
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.order.update({
|
||||
@@ -229,6 +235,8 @@ export class TradeService {
|
||||
payStatus: 'PAID',
|
||||
paidAt: now,
|
||||
payExternalNo: externalNo,
|
||||
partnerAccountIdAtPay: paySnapshot?.partnerAccountId ?? null,
|
||||
orderCommissionRateAtPay: paySnapshot?.orderCommissionRate ?? null,
|
||||
},
|
||||
});
|
||||
await tx.logThirdParty.create({
|
||||
@@ -300,6 +308,10 @@ export class TradeService {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const paySnapshot = await this.partnerCityService.resolveForOrder(
|
||||
order.cityId,
|
||||
order.receiverDistrict,
|
||||
);
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const current = await tx.order.findUnique({ where: { id: order.id } });
|
||||
if (!current || current.payStatus === 'PAID') return;
|
||||
@@ -311,6 +323,8 @@ export class TradeService {
|
||||
payStatus: 'PAID',
|
||||
paidAt: now,
|
||||
payExternalNo: params.transactionId,
|
||||
partnerAccountIdAtPay: paySnapshot?.partnerAccountId ?? null,
|
||||
orderCommissionRateAtPay: paySnapshot?.orderCommissionRate ?? null,
|
||||
},
|
||||
});
|
||||
await tx.logThirdParty.create({
|
||||
@@ -450,15 +464,10 @@ export class TradeService {
|
||||
}
|
||||
|
||||
async listPartnerReshipments(partnerAccountId: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
const cities = await this.prisma.commonCity.findMany({
|
||||
where: { partnerId: account.partnerId },
|
||||
select: { id: true },
|
||||
});
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const orderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: { cityId: { in: cities.map((c) => c.id) } },
|
||||
where: orderWhere,
|
||||
select: { id: true },
|
||||
});
|
||||
const tickets = await this.prisma.commonTicket.findMany({
|
||||
@@ -473,12 +482,8 @@ export class TradeService {
|
||||
}
|
||||
|
||||
async listPartnerOrders(partnerAccountId: bigint, page = 1, pageSize = 20) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
const cities = await this.prisma.commonCity.findMany({ where: { partnerId: account.partnerId } });
|
||||
const cityIds = cities.map((c) => c.id);
|
||||
const where = { cityId: { in: cityIds } };
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const where = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||
const [list, total] = await Promise.all([
|
||||
this.prisma.order.findMany({
|
||||
where,
|
||||
@@ -493,12 +498,10 @@ export class TradeService {
|
||||
}
|
||||
|
||||
async getPartnerOrder(partnerAccountId: bigint, orderId: bigint) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
const cities = await this.prisma.commonCity.findMany({ where: { partnerId: account.partnerId } });
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const partnerOrderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||
const order = await this.prisma.order.findFirst({
|
||||
where: { id: orderId, cityId: { in: cities.map((c) => c.id) } },
|
||||
where: { id: orderId, ...partnerOrderWhere },
|
||||
include: { delivery: true, user: true, imageResource: true },
|
||||
});
|
||||
if (!order) throw new NotFoundException('订单不存在');
|
||||
@@ -510,13 +513,12 @@ export class TradeService {
|
||||
}
|
||||
|
||||
async advanceDelivery(partnerAccountId: bigint, orderId: bigint, targetStatus: string) {
|
||||
const account = await this.prisma.partnerAccount.findUniqueOrThrow({
|
||||
where: { id: partnerAccountId },
|
||||
});
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const partnerOrderWhere = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
|
||||
const order = await this.prisma.order.findFirst({
|
||||
where: {
|
||||
id: orderId,
|
||||
city: { partnerId: account.partnerId },
|
||||
...partnerOrderWhere,
|
||||
},
|
||||
include: { delivery: true },
|
||||
});
|
||||
@@ -524,7 +526,7 @@ export class TradeService {
|
||||
await this.applyStatusTransition(order.id, order.status, targetStatus);
|
||||
if (targetStatus === 'SHIPPING') {
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||
partnerId: account.partnerId,
|
||||
partnerAccountId: primary.id,
|
||||
eventName: 'partner_order_ship',
|
||||
refType: 'ORDER',
|
||||
refId: orderId,
|
||||
@@ -532,7 +534,7 @@ export class TradeService {
|
||||
});
|
||||
}
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||
partnerId: account.partnerId,
|
||||
partnerAccountId: primary.id,
|
||||
eventName: 'partner_delivery_advance',
|
||||
refType: 'ORDER',
|
||||
refId: orderId,
|
||||
|
||||
Reference in New Issue
Block a user