merge: release approved store account updates
This commit is contained in:
commit
ea363984a7
@ -0,0 +1,22 @@
|
||||
ALTER TABLE "StoreAccount"
|
||||
ADD COLUMN "subject" TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN "boundBankAccountNumberCiphertext" TEXT NOT NULL DEFAULT '';
|
||||
|
||||
CREATE TABLE "StoreAccountStoreProfile" (
|
||||
"id" TEXT NOT NULL,
|
||||
"storeName" TEXT NOT NULL,
|
||||
"platform" TEXT NOT NULL,
|
||||
"merchantNumber" TEXT NOT NULL DEFAULT '',
|
||||
"secondaryMerchantNumber" TEXT NOT NULL DEFAULT '',
|
||||
"merchantStoreId" TEXT NOT NULL DEFAULT '',
|
||||
"companyId" TEXT NOT NULL DEFAULT '',
|
||||
"createdById" TEXT,
|
||||
"updatedById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
CONSTRAINT "StoreAccountStoreProfile_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "StoreAccountStoreProfile_storeName_platform_key" ON "StoreAccountStoreProfile"("storeName", "platform");
|
||||
CREATE INDEX "StoreAccountStoreProfile_storeName_idx" ON "StoreAccountStoreProfile"("storeName");
|
||||
CREATE INDEX "StoreAccountStoreProfile_platform_idx" ON "StoreAccountStoreProfile"("platform");
|
||||
@ -3499,6 +3499,8 @@ model StoreAccount {
|
||||
verificationContact String @default("")
|
||||
jstOrderService String @default("")
|
||||
accountGroupName String @default("")
|
||||
subject String @default("")
|
||||
boundBankAccountNumberCiphertext String @default("") @db.Text
|
||||
createdById String?
|
||||
updatedById String?
|
||||
createdAt DateTime @default(now())
|
||||
@ -3510,7 +3512,23 @@ model StoreAccount {
|
||||
@@index([accountType])
|
||||
@@index([updatedAt])
|
||||
}
|
||||
model StoreAccountStoreProfile {
|
||||
id String @id @default(cuid())
|
||||
storeName String
|
||||
platform String
|
||||
merchantNumber String @default("")
|
||||
secondaryMerchantNumber String @default("")
|
||||
merchantStoreId String @default("")
|
||||
companyId String @default("")
|
||||
createdById String?
|
||||
updatedById String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([storeName, platform])
|
||||
@@index([storeName])
|
||||
@@index([platform])
|
||||
}
|
||||
model StoreAccountChangeLog {
|
||||
id String @id @default(cuid())
|
||||
storeAccountId String?
|
||||
@ -4042,4 +4060,3 @@ model ScriptMonitorRun {
|
||||
@@index([scriptId, startedAt(sort: Desc)])
|
||||
@@index([agentId, startedAt(sort: Desc)])
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { handleRequest, jsonSuccess } from '@/server/core/response'
|
||||
import { storeAccountManagementService } from '@/server/services/store-account-management.service'
|
||||
|
||||
type RouteProps = { params: Promise<{ id: string }> }
|
||||
|
||||
export async function GET(request: NextRequest, props: RouteProps) {
|
||||
return handleRequest(request, async (ctx) => {
|
||||
const { id } = await props.params
|
||||
return jsonSuccess(await storeAccountManagementService.revealBankAccount(id, ctx.user), 200, ctx.requestId)
|
||||
}, { requireAuth: true })
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import * as XLSX from 'xlsx'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
|
||||
import { storeAccountManagementService } from '@/server/services/store-account-management.service'
|
||||
import { STORE_ACCOUNT_PROFILE_FIELDS } from '@/lib/store-account-management'
|
||||
|
||||
const HEADER_MAP: Record<string, string> = Object.fromEntries([
|
||||
['店铺名称', 'storeName'],
|
||||
...STORE_ACCOUNT_PROFILE_FIELDS.map((field) => [field.label, field.key]),
|
||||
])
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return handleRequest(request, async (ctx) => {
|
||||
const form = await request.formData()
|
||||
const file = form.get('file')
|
||||
const platform = String(form.get('platform') ?? '')
|
||||
if (!(file instanceof File)) return jsonError(ErrorCode.VALIDATION_REQUIRED_FIELD, '请选择 Excel 文件', 400, ctx.requestId)
|
||||
if (file.size > 10 * 1024 * 1024) return jsonError(ErrorCode.VALIDATION_INVALID_FORMAT, 'Excel 文件不能超过10MB', 400, ctx.requestId)
|
||||
const workbook = XLSX.read(Buffer.from(await file.arrayBuffer()), { type: 'buffer', cellDates: false })
|
||||
const sheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||
if (!sheet) return jsonError(ErrorCode.VALIDATION_INVALID_FORMAT, 'Excel 文件没有工作表', 400, ctx.requestId)
|
||||
const rawRows = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheet, { defval: '' })
|
||||
if (rawRows.length === 0) return jsonError(ErrorCode.VALIDATION_REQUIRED_FIELD, '导入文件没有可用数据', 400, ctx.requestId)
|
||||
if (rawRows.length > 5000) return jsonError(ErrorCode.VALIDATION_INVALID_FORMAT, '单次最多导入5000行', 400, ctx.requestId)
|
||||
let processed = 0
|
||||
for (const raw of rawRows) {
|
||||
const mapped = Object.fromEntries(Object.entries(raw).flatMap(([header, value]) => {
|
||||
const key = HEADER_MAP[header.trim()]
|
||||
return key ? [[key, value]] : []
|
||||
}))
|
||||
const storeName = String(mapped.storeName ?? '').trim()
|
||||
if (!storeName) throw new Error(`第${processed + 2}行缺少店铺名称`)
|
||||
await storeAccountManagementService.saveStoreProfile({ storeName, platform, values: mapped, user: ctx.user })
|
||||
processed += 1
|
||||
}
|
||||
return jsonSuccess({ processed, total: rawRows.length }, 200, ctx.requestId)
|
||||
}, { requireAuth: true })
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
|
||||
import { storeAccountManagementService } from '@/server/services/store-account-management.service'
|
||||
|
||||
const schema = z.object({
|
||||
storeName: z.string().trim().min(1).max(240),
|
||||
platform: z.string().trim().min(1).max(10),
|
||||
values: z.record(z.string(), z.unknown()).default({}),
|
||||
}).strict()
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return handleRequest(request, async (ctx) => {
|
||||
const storeName = request.nextUrl.searchParams.get('storeName') ?? ''
|
||||
return jsonSuccess(await storeAccountManagementService.listStoreProfiles(storeName, ctx.user), 200, ctx.requestId)
|
||||
}, { requireAuth: true })
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
return handleRequest(request, async (ctx) => {
|
||||
const parsed = schema.safeParse(await request.json().catch(() => null))
|
||||
if (!parsed.success) return jsonError(ErrorCode.VALIDATION_INVALID_FORMAT, '店铺资料内容不正确', 400, ctx.requestId)
|
||||
return jsonSuccess(await storeAccountManagementService.saveStoreProfile({ ...parsed.data, user: ctx.user }), 200, ctx.requestId)
|
||||
}, { requireAuth: true })
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import ExcelJS from 'exceljs'
|
||||
import { STORE_ACCOUNT_PROFILE_FIELDS, STORE_ACCOUNT_PROFILE_PLATFORMS, type StoreAccountProfilePlatform } from '@/lib/store-account-management'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
import { handleRequest, jsonError } from '@/server/core/response'
|
||||
import { storeAccountManagementAccessService } from '@/server/services/store-account-management-access.service'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return handleRequest(request, async (ctx) => {
|
||||
const capabilities = await storeAccountManagementAccessService.getCapabilities(ctx.user)
|
||||
if (!capabilities.pageAccess.edit) return jsonError(ErrorCode.AUTH_PERMISSION_DENIED, '当前账号没有编辑店铺资料的权限', 403, ctx.requestId)
|
||||
const platform = request.nextUrl.searchParams.get('platform')?.toUpperCase() as StoreAccountProfilePlatform
|
||||
const platformLabel = STORE_ACCOUNT_PROFILE_PLATFORMS.find((item) => item.key === platform)?.label
|
||||
if (!platformLabel) return jsonError(ErrorCode.VALIDATION_INVALID_FORMAT, '平台只能是京东或京喜', 400, ctx.requestId)
|
||||
const workbook = new ExcelJS.Workbook()
|
||||
const sheet = workbook.addWorksheet(`${platformLabel}店铺资料`)
|
||||
sheet.columns = [{ header: '店铺名称', key: 'storeName', width: 28 }, ...STORE_ACCOUNT_PROFILE_FIELDS.map((field) => ({ header: field.label, key: field.key, width: 28 }))]
|
||||
sheet.views = [{ state: 'frozen', ySplit: 1 }]
|
||||
sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } }
|
||||
sheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF27272A' } }
|
||||
const content = Buffer.from(await workbook.xlsx.writeBuffer())
|
||||
return new NextResponse(new Uint8Array(content), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(`${platformLabel}店铺资料导入模板.xlsx`)}`,
|
||||
'Cache-Control': 'private, no-store, max-age=0',
|
||||
},
|
||||
})
|
||||
}, { requireAuth: true })
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { RequireAuth } from '@/components/auth/RequireAuth'
|
||||
import { PermissionGuard } from '@/components/auth/PermissionGuard'
|
||||
import { UserPermissionCenter } from './components/UserPermissionCenter'
|
||||
|
||||
export function UserPermissionManagementPage({
|
||||
function UserPermissionManagementPage({
|
||||
backHref: _backHref = '/intake',
|
||||
backLabel: _backLabel = '产品开发',
|
||||
}: {
|
||||
|
||||
@ -30,3 +30,6 @@ export function decryptStoreAccountPassword(value: string) {
|
||||
decipher.setAuthTag(Buffer.from(tagPart, 'base64url'))
|
||||
return Buffer.concat([decipher.update(Buffer.from(dataPart, 'base64url')), decipher.final()]).toString('utf8')
|
||||
}
|
||||
|
||||
export const encryptStoreAccountBankAccount = encryptStoreAccountPassword
|
||||
export const decryptStoreAccountBankAccount = decryptStoreAccountPassword
|
||||
|
||||
@ -15,6 +15,20 @@ export type StoreAccountRecord = {
|
||||
verificationContact: string
|
||||
jstOrderService: string
|
||||
accountGroupName: string
|
||||
subject: string
|
||||
boundBankAccountNumberCiphertext: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type StoreAccountProfileRecord = {
|
||||
id: string
|
||||
storeName: string
|
||||
platform: string
|
||||
merchantNumber: string
|
||||
secondaryMerchantNumber: string
|
||||
merchantStoreId: string
|
||||
companyId: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
@ -50,6 +64,20 @@ const ACCOUNT_SELECT = {
|
||||
verificationContact: true,
|
||||
jstOrderService: true,
|
||||
accountGroupName: true,
|
||||
subject: true,
|
||||
boundBankAccountNumberCiphertext: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
} as const
|
||||
|
||||
const PROFILE_SELECT = {
|
||||
id: true,
|
||||
storeName: true,
|
||||
platform: true,
|
||||
merchantNumber: true,
|
||||
secondaryMerchantNumber: true,
|
||||
merchantStoreId: true,
|
||||
companyId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
} as const
|
||||
@ -158,6 +186,42 @@ export class StoreAccountManagementRepository {
|
||||
})
|
||||
}
|
||||
|
||||
async listProfiles(storeName: string) {
|
||||
return prisma.storeAccountStoreProfile.findMany({
|
||||
where: { storeName },
|
||||
select: PROFILE_SELECT,
|
||||
orderBy: { platform: 'asc' },
|
||||
})
|
||||
}
|
||||
|
||||
async upsertProfile(data: {
|
||||
storeName: string
|
||||
platform: string
|
||||
values: Record<string, string>
|
||||
actorId: string
|
||||
}) {
|
||||
return prisma.storeAccountStoreProfile.upsert({
|
||||
where: {
|
||||
storeName_platform: {
|
||||
storeName: data.storeName,
|
||||
platform: data.platform,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
...data.values,
|
||||
storeName: data.storeName,
|
||||
platform: data.platform,
|
||||
createdById: data.actorId,
|
||||
updatedById: data.actorId,
|
||||
},
|
||||
update: {
|
||||
...data.values,
|
||||
updatedById: data.actorId,
|
||||
},
|
||||
select: PROFILE_SELECT,
|
||||
})
|
||||
}
|
||||
|
||||
async upsertByKey(data: {
|
||||
storeName: string
|
||||
accountType: string
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user