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("")
|
verificationContact String @default("")
|
||||||
jstOrderService String @default("")
|
jstOrderService String @default("")
|
||||||
accountGroupName String @default("")
|
accountGroupName String @default("")
|
||||||
|
subject String @default("")
|
||||||
|
boundBankAccountNumberCiphertext String @default("") @db.Text
|
||||||
createdById String?
|
createdById String?
|
||||||
updatedById String?
|
updatedById String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -3510,7 +3512,23 @@ model StoreAccount {
|
|||||||
@@index([accountType])
|
@@index([accountType])
|
||||||
@@index([updatedAt])
|
@@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 {
|
model StoreAccountChangeLog {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
storeAccountId String?
|
storeAccountId String?
|
||||||
@ -4042,4 +4060,3 @@ model ScriptMonitorRun {
|
|||||||
@@index([scriptId, startedAt(sort: Desc)])
|
@@index([scriptId, startedAt(sort: Desc)])
|
||||||
@@index([agentId, 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 { PermissionGuard } from '@/components/auth/PermissionGuard'
|
||||||
import { UserPermissionCenter } from './components/UserPermissionCenter'
|
import { UserPermissionCenter } from './components/UserPermissionCenter'
|
||||||
|
|
||||||
export function UserPermissionManagementPage({
|
function UserPermissionManagementPage({
|
||||||
backHref: _backHref = '/intake',
|
backHref: _backHref = '/intake',
|
||||||
backLabel: _backLabel = '产品开发',
|
backLabel: _backLabel = '产品开发',
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -30,3 +30,6 @@ export function decryptStoreAccountPassword(value: string) {
|
|||||||
decipher.setAuthTag(Buffer.from(tagPart, 'base64url'))
|
decipher.setAuthTag(Buffer.from(tagPart, 'base64url'))
|
||||||
return Buffer.concat([decipher.update(Buffer.from(dataPart, 'base64url')), decipher.final()]).toString('utf8')
|
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
|
verificationContact: string
|
||||||
jstOrderService: string
|
jstOrderService: string
|
||||||
accountGroupName: 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
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
@ -50,6 +64,20 @@ const ACCOUNT_SELECT = {
|
|||||||
verificationContact: true,
|
verificationContact: true,
|
||||||
jstOrderService: true,
|
jstOrderService: true,
|
||||||
accountGroupName: 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,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
} as const
|
} 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: {
|
async upsertByKey(data: {
|
||||||
storeName: string
|
storeName: string
|
||||||
accountType: string
|
accountType: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user