mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 10:00:30 +08:00
feat: add user management CRUD and platform_user table
V24 migration creates platform_user table. Backend UserAdminController provides list/create/update/toggleStatus. Frontend UserManagementView enables admin to add/edit/disable users. Replaces hardcoded auth with database-backed user lifecycle. Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package cn.craftlabs.platform.api.persistence.auth;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
@TableName("platform_user")
|
||||||
|
public class PlatformUser {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@TableField("username")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@TableField("display_name")
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
@TableField("password_hash")
|
||||||
|
private String passwordHash;
|
||||||
|
|
||||||
|
@TableField("role")
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
@TableField("status")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@TableField("created_at")
|
||||||
|
private OffsetDateTime createdAt;
|
||||||
|
|
||||||
|
@TableField("updated_at")
|
||||||
|
private OffsetDateTime updatedAt;
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||||
|
|
||||||
|
public String getPasswordHash() { return passwordHash; }
|
||||||
|
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
|
||||||
|
|
||||||
|
public String getRole() { return role; }
|
||||||
|
public void setRole(String role) { this.role = role; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public OffsetDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package cn.craftlabs.platform.api.persistence.auth;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface PlatformUserMapper extends BaseMapper<PlatformUser> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- V24__platform_user.sql
|
||||||
|
-- 用户与账号生命周期(M11-F14),替代 AuthController 中硬编码的 4 个用户
|
||||||
|
-- BCrypt 哈希由 python3 -c "import bcrypt; print(bcrypt.hashpw(b'admin', bcrypt.gensalt(10)))" 生成
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS platform_user (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
display_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
password_hash VARCHAR(256) NOT NULL,
|
||||||
|
role VARCHAR(32) NOT NULL DEFAULT 'SALES',
|
||||||
|
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE platform_user IS '平台用户(M11-F14)';
|
||||||
|
COMMENT ON COLUMN platform_user.username IS '登录名';
|
||||||
|
COMMENT ON COLUMN platform_user.password_hash IS 'BCrypt 哈希';
|
||||||
|
COMMENT ON COLUMN platform_user.role IS '角色代码,与 PlatformRoles 一致';
|
||||||
|
COMMENT ON COLUMN platform_user.status IS 'ACTIVE=正常 DISABLED=禁用 ARCHIVED=归档';
|
||||||
|
|
||||||
|
INSERT INTO platform_user (username, display_name, password_hash, role, status) VALUES
|
||||||
|
('admin', '管理员', '$2b$10$SWAtb2IcPL9C2NOOIl/mFOOVGGxHzgOWAqc6TpsP5TJNvjRQezr4e', 'SYS_ADMIN', 'ACTIVE'),
|
||||||
|
('sales', '销售账号', '$2b$10$HoUyBcoXb9xe1tsqYPxhc.eNKdWDKK.7KtXIti/pJscBxnkIUrqmK', 'SALES', 'ACTIVE'),
|
||||||
|
('delivery', '交付账号', '$2b$10$jPoVcLPx3o6TIQmAg3WGXe8.41xr.q.ySDTGgNwwGZ8OiAA5xwoai', 'DELIVERY', 'ACTIVE'),
|
||||||
|
('ops', '运营账号', '$2b$10$.gQu/dv.m2S9uYuqZc1ymeEiRKa0j4dhWjzEF.e0GApFKmUdIhos6', 'LICENSE_OPS', 'ACTIVE')
|
||||||
|
ON CONFLICT (username) DO NOTHING;
|
||||||
@@ -461,6 +461,20 @@ export function createSkuMapping(contractLineId, body) { return axios.post(`/api
|
|||||||
export function updateSkuMapping(id, body) { return axios.put(`/api/v1/integration/sku-mappings/${id}`, body); }
|
export function updateSkuMapping(id, body) { return axios.put(`/api/v1/integration/sku-mappings/${id}`, body); }
|
||||||
export function deleteSkuMapping(id) { return axios.delete(`/api/v1/integration/sku-mappings/${id}`); }
|
export function deleteSkuMapping(id) { return axios.delete(`/api/v1/integration/sku-mappings/${id}`); }
|
||||||
|
|
||||||
|
// —— M11-F14 用户管理 ————————————————————————————
|
||||||
|
export function listUsers() {
|
||||||
|
return axios.get('/api/v1/admin/users');
|
||||||
|
}
|
||||||
|
export function createUser(body) {
|
||||||
|
return axios.post('/api/v1/admin/users', body);
|
||||||
|
}
|
||||||
|
export function updateUser(id, body) {
|
||||||
|
return axios.put(`/api/v1/admin/users/${id}`, body);
|
||||||
|
}
|
||||||
|
export function patchUserStatus(id, body) {
|
||||||
|
return axios.patch(`/api/v1/admin/users/${id}/status`, body);
|
||||||
|
}
|
||||||
|
|
||||||
export function listStakeholders(projectId) {
|
export function listStakeholders(projectId) {
|
||||||
return axios.get(`/api/v1/projects/${projectId}/stakeholders`);
|
return axios.get(`/api/v1/projects/${projectId}/stakeholders`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { apiErrorMessage } from '../utils/apiErrorMessage'
|
||||||
|
import { listUsers, createUser, updateUser, patchUserStatus } from '../api/platform'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const users = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
const editingId = ref(null)
|
||||||
|
const form = ref({ username: '', displayName: '', password: '', role: 'SALES' })
|
||||||
|
|
||||||
|
const roles = [
|
||||||
|
{ value: 'SYS_ADMIN', label: '系统管理员' },
|
||||||
|
{ value: 'SALES', label: '商务经理' },
|
||||||
|
{ value: 'DELIVERY', label: '交付工程师' },
|
||||||
|
{ value: 'LICENSE_OPS', label: '授权运营' },
|
||||||
|
]
|
||||||
|
|
||||||
|
onMounted(loadData)
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await listUsers()
|
||||||
|
users.value = data
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, '加载用户列表失败'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
editingId.value = null
|
||||||
|
dialogTitle.value = '新建用户'
|
||||||
|
form.value = { username: '', displayName: '', password: '', role: 'SALES' }
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(row) {
|
||||||
|
editingId.value = row.id
|
||||||
|
dialogTitle.value = '编辑用户'
|
||||||
|
form.value = {
|
||||||
|
username: row.username,
|
||||||
|
displayName: row.displayName || '',
|
||||||
|
password: '',
|
||||||
|
role: row.role || 'SALES',
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
const body = { ...form.value }
|
||||||
|
if (!editingId.value) {
|
||||||
|
if (!body.password || body.password.length < 6) {
|
||||||
|
ElMessage.error('密码至少6位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await createUser(body)
|
||||||
|
ElMessage.success('用户创建成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
await loadData()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, '创建失败'))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (body.password && body.password.length < 6) {
|
||||||
|
ElMessage.error('密码至少6位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateUser(editingId.value, body)
|
||||||
|
ElMessage.success('用户更新成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
await loadData()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, '更新失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggleStatus(row) {
|
||||||
|
const newStatus = row.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'
|
||||||
|
const label = newStatus === 'ACTIVE' ? '启用' : '禁用'
|
||||||
|
try {
|
||||||
|
await patchUserStatus(row.id, { status: newStatus })
|
||||||
|
ElMessage.success(`用户已${label}`)
|
||||||
|
await loadData()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, `${label}失败`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTagType(status) {
|
||||||
|
return status === 'ACTIVE' ? 'success' : 'danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status) {
|
||||||
|
return status === 'ACTIVE' ? '正常' : '已禁用'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>用户管理</h2>
|
||||||
|
<el-card shadow="never" style="margin-top: 16px">
|
||||||
|
<template #header>
|
||||||
|
<el-button type="primary" @click="openCreate">新建用户</el-button>
|
||||||
|
</template>
|
||||||
|
<el-table v-loading="loading" :data="users" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="username" label="用户名" min-width="140" />
|
||||||
|
<el-table-column prop="displayName" label="显示名" min-width="160" />
|
||||||
|
<el-table-column label="角色" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small">{{ row.role }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="openEdit(row)">编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
:type="row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||||
|
link
|
||||||
|
@click="handleToggleStatus(row)"
|
||||||
|
>
|
||||||
|
{{ row.status === 'ACTIVE' ? '禁用' : '启用' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="480px" @closed="dialogVisible = false">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="用户名" required>
|
||||||
|
<el-input v-model="form.username" :disabled="!!editingId" maxlength="64" placeholder="登录名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="显示名">
|
||||||
|
<el-input v-model="form.displayName" maxlength="128" placeholder="选填,默认同用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色" required>
|
||||||
|
<el-select v-model="form.role" style="width: 100%">
|
||||||
|
<el-option v-for="r in roles" :key="r.value" :label="r.label" :value="r.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="editingId ? '新密码' : '密码'" :required="!editingId">
|
||||||
|
<el-input v-model="form.password" type="password" show-password :placeholder="editingId ? '留空则不修改' : '至少6位'" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user