feat: 平台管理员创建企业客户功能 + deploy.sh 跳过 tsc

This commit is contained in:
selfrelease
2026-07-29 11:54:23 +08:00
parent 987a4678f7
commit b2248b9afc
3 changed files with 168 additions and 10 deletions
+65
View File
@@ -136,6 +136,71 @@ router.get('/orgs', async (req: AuthRequest, res, next) => {
}
})
/**
* 创建企业客户(含管理员账号)
*/
router.post('/orgs', async (req: AuthRequest, res, next) => {
try {
const {
name, plan, maxEmployees, city, contactName, contactPhone,
adminName, adminPhone, adminPassword,
} = req.body as {
name: string; plan?: string; maxEmployees?: number
city?: string; contactName?: string; contactPhone?: string
adminName?: string; adminPhone: string; adminPassword: string
}
if (!name || !adminPhone || !adminPassword) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '企业名称、管理员手机号、密码不能为空' } })
}
if (adminPassword.length < 8) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '密码至少8位' } })
}
const existing = await prisma.user.findUnique({ where: { phone: adminPhone } })
if (existing) {
return res.status(400).json({ success: false, error: { code: 'DUPLICATE', message: '该手机号已存在' } })
}
// 创建企业
const org = await prisma.organization.create({
data: {
name,
plan: (plan as any) || 'FREE',
maxEmployees: maxEmployees || 20,
city: city || null,
contactName: contactName || null,
contactPhone: contactPhone || null,
},
})
// 创建管理员账号
const bcrypt = await import('bcryptjs')
const passwordHash = await bcrypt.default.hash(adminPassword, 10)
const admin = await prisma.user.create({
data: {
orgId: org.id,
phone: adminPhone,
name: adminName || '管理员',
passwordHash,
role: 'ADMIN',
},
select: { id: true, name: true, phone: true, role: true },
})
res.json({
success: true,
data: {
org: { id: org.id, name: org.name, plan: org.plan, maxEmployees: org.maxEmployees, city: org.city },
admin,
},
})
} catch (err) {
next(err)
}
})
/**
* 企业详情
*/
+2 -9
View File
@@ -197,15 +197,8 @@ build_backend() {
log "生成 Prisma Client..."
npx prisma generate
log "编译 TypeScript..."
npx tsc
if [ ! -d dist ]; then
err "后端编译失败,dist 目录不存在"
exit 1
fi
log "后端编译完成: dist/ ($(du -sh dist | cut -f1))"
# 服务器用 tsx 直接运行 TS 源码,跳过 tsc 编译
log "跳过 tsc 编译(服务器使用 tsx 运行)"
}
# ===========================================
+101 -1
View File
@@ -2,7 +2,7 @@
* 企业租户管理页 — 列表、搜索、查看详情、编辑套餐、删除
*/
import { useEffect, useState } from 'react'
import { Search, Building2, Eye, Trash2, Edit2 } from 'lucide-react'
import { Search, Building2, Eye, Trash2, Edit2, Plus } from 'lucide-react'
import api from '../../lib/api'
import { Input, Select, Label } from '../../components/ui/Input'
import Button from '../../components/ui/Button'
@@ -28,6 +28,13 @@ export default function PlatformOrgs() {
const [loading, setLoading] = useState(true)
const [editOrg, setEditOrg] = useState<Org | null>(null)
const [deleteOrg, setDeleteOrg] = useState<Org | null>(null)
const [createOpen, setCreateOpen] = useState(false)
const [createForm, setCreateForm] = useState({
name: '', plan: 'FREE', maxEmployees: 20, city: '',
contactName: '', contactPhone: '',
adminName: '管理员', adminPhone: '', adminPassword: '12345678',
})
const [creating, setCreating] = useState(false)
const fetchOrgs = async () => {
setLoading(true)
@@ -66,6 +73,28 @@ export default function PlatformOrgs() {
}
}
const handleCreate = async () => {
if (!createForm.name || !createForm.adminPhone || !createForm.adminPassword) {
alert('企业名称、管理员手机号、密码不能为空')
return
}
setCreating(true)
try {
await api.post('/platform/orgs', createForm)
setCreateOpen(false)
setCreateForm({
name: '', plan: 'FREE', maxEmployees: 20, city: '',
contactName: '', contactPhone: '',
adminName: '管理员', adminPhone: '', adminPassword: '12345678',
})
fetchOrgs()
} catch (err: any) {
alert(err.response?.data?.error?.message || '创建失败')
} finally {
setCreating(false)
}
}
const handleDelete = async () => {
if (!deleteOrg) return
try {
@@ -109,6 +138,10 @@ export default function PlatformOrgs() {
<Button variant="secondary" onClick={fetchOrgs} className="bg-slate-800 text-slate-200 border border-slate-700 hover:bg-slate-700">
</Button>
<Button onClick={() => setCreateOpen(true)} className="bg-amber-400 text-slate-950 hover:bg-amber-300">
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
{/* 表格 */}
@@ -182,6 +215,73 @@ export default function PlatformOrgs() {
)}
</div>
{/* 创建企业弹窗 */}
{createOpen && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setCreateOpen(false)}>
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
<h2 className="text-lg font-semibold text-white mb-4"></h2>
<div className="space-y-3">
<div className="border-b border-slate-700 pb-3">
<h3 className="text-xs text-amber-400 font-medium mb-3"></h3>
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2">
<Label className="text-slate-300"> *</Label>
<Input className="bg-slate-800 border-slate-700 text-white" placeholder="如:北京XX科技有限公司" value={createForm.name} onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })} />
</div>
<div>
<Label className="text-slate-300"></Label>
<Select className="bg-slate-800 border-slate-700 text-white" value={createForm.plan} onChange={(e) => setCreateForm({ ...createForm, plan: e.target.value })}>
<option value="FREE"></option>
<option value="PRO"></option>
<option value="ENTERPRISE"></option>
</Select>
</div>
<div>
<Label className="text-slate-300"></Label>
<Input type="number" className="bg-slate-800 border-slate-700 text-white" value={createForm.maxEmployees} onChange={(e) => setCreateForm({ ...createForm, maxEmployees: parseInt(e.target.value) || 0 })} />
</div>
<div>
<Label className="text-slate-300"></Label>
<Input className="bg-slate-800 border-slate-700 text-white" placeholder="如:北京" value={createForm.city} onChange={(e) => setCreateForm({ ...createForm, city: e.target.value })} />
</div>
<div>
<Label className="text-slate-300"></Label>
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.contactName} onChange={(e) => setCreateForm({ ...createForm, contactName: e.target.value })} />
</div>
<div className="col-span-2">
<Label className="text-slate-300"></Label>
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.contactPhone} onChange={(e) => setCreateForm({ ...createForm, contactPhone: e.target.value })} />
</div>
</div>
</div>
<div className="border-b border-slate-700 pb-3">
<h3 className="text-xs text-amber-400 font-medium mb-3"></h3>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-slate-300"></Label>
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.adminName} onChange={(e) => setCreateForm({ ...createForm, adminName: e.target.value })} />
</div>
<div>
<Label className="text-slate-300"> *</Label>
<Input className="bg-slate-800 border-slate-700 text-white" placeholder="13800000000" maxLength={11} value={createForm.adminPhone} onChange={(e) => setCreateForm({ ...createForm, adminPhone: e.target.value })} />
</div>
<div className="col-span-2">
<Label className="text-slate-300"> *8</Label>
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.adminPassword} onChange={(e) => setCreateForm({ ...createForm, adminPassword: e.target.value })} />
</div>
</div>
</div>
<div className="flex gap-2 pt-2">
<Button className="bg-amber-400 text-slate-950 hover:bg-amber-300 flex-1" onClick={handleCreate} disabled={creating}>
{creating ? '创建中...' : '确认创建'}
</Button>
<Button variant="secondary" className="bg-slate-800 text-slate-200 border border-slate-700" onClick={() => setCreateOpen(false)}></Button>
</div>
</div>
</div>
</div>
)}
{/* 编辑弹窗 */}
{editOrg && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setEditOrg(null)}>