feat(admin): 账号区域标识回填

This commit is contained in:
Codex Agent 2025-11-27 17:13:49 +08:00
parent 34bce0f5df
commit 64585261c4
4 changed files with 143 additions and 7 deletions

View File

@ -55,6 +55,7 @@ def _scrub_user_payload(user: Dict[str, Any]) -> Dict[str, Any]:
"name": user.get("service_department_name"),
"code": user.get("service_department_code"),
"region_id": user.get("service_department_region_id"),
"region_name": user.get("service_department_region_name"),
"parent_id": user.get("service_department_parent_id"),
"phone": user.get("service_department_phone"),
"role": user.get("department_role"),

View File

@ -275,11 +275,23 @@ def admin_create_user():
return jsonify({"success": False, "message": "用户名和密码均不能为空"}), 400
parent_department_id = (payload.get("parent_department_id") or "").strip() or None
service_department_id = (payload.get("service_department_id") or "").strip() or None
region_id = (payload.get("region_id") or "").strip() or None
department_phone = (payload.get("department_phone") or "").strip() or None
# 如果未显式绑定部门,则为该用户创建一个同名单位,并按父级决定层级
created_department: Optional[Dict[str, Any]] = None
if not service_department_id:
# 未显式指定区域时,继承父级部门的区域
if not region_id and parent_department_id:
try:
parent_dept = next(
(dept for dept in list_service_departments() if str(dept.get("id")) == parent_department_id),
None
)
if parent_dept:
region_id = parent_dept.get("region_id")
except Exception:
region_id = None
try:
dept_name = (payload.get("display_name") or username).strip() or username
dept_code = username.upper()
@ -288,6 +300,7 @@ def admin_create_user():
code=dept_code,
phone=department_phone,
parent_id=parent_department_id,
region_id=region_id,
)
service_department_id = created_department.get("id")
except Exception as exc:
@ -303,6 +316,7 @@ def admin_create_user():
service_department_id=service_department_id,
department_role=payload.get("department_role"),
parent_department_id=parent_department_id,
service_department_region_id=region_id,
service_department_phone=department_phone,
)
return jsonify({"success": True, "data": {"user": user, "department": created_department}})
@ -508,6 +522,16 @@ def admin_create_service_department():
"description": payload.get("description"),
"grade": grade,
}
if not kwargs["region_id"] and kwargs["parent_id"]:
try:
parent_dept = next(
(dept for dept in list_service_departments() if str(dept.get("id")) == kwargs["parent_id"]),
None
)
if parent_dept:
kwargs["region_id"] = parent_dept.get("region_id")
except Exception:
kwargs["region_id"] = None
try:
department = create_service_department(name, code=code_token, **kwargs)
department_id = department.get("id")

View File

@ -279,6 +279,7 @@ def _public_user_payload(user: Dict[str, Any]) -> Dict[str, Any]:
"phone": user.get("service_department_phone"),
"parent_id": user.get("service_department_parent_id"),
"region_id": user.get("service_department_region_id"),
"region_name": user.get("service_department_region_name"),
"role": user.get("department_role"),
}
created_at = user.get("created_at")
@ -322,9 +323,11 @@ def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
sd.code AS service_department_code,
sd.phone AS service_department_phone,
sd.parent_id AS service_department_parent_id,
sd.region_id AS service_department_region_id
sd.region_id AS service_department_region_id,
r.name AS service_department_region_name
FROM auth_users au
LEFT JOIN service_departments sd ON sd.id = au.service_department_id
LEFT JOIN regions r ON r.id = sd.region_id
WHERE au.username = %s
LIMIT 1
""",
@ -359,9 +362,11 @@ def get_user_by_id(user_id: str) -> Optional[Dict[str, Any]]:
sd.code AS service_department_code,
sd.phone AS service_department_phone,
sd.parent_id AS service_department_parent_id,
sd.region_id AS service_department_region_id
sd.region_id AS service_department_region_id,
r.name AS service_department_region_name
FROM auth_users au
LEFT JOIN service_departments sd ON sd.id = au.service_department_id
LEFT JOIN regions r ON r.id = sd.region_id
WHERE au.id = %s
LIMIT 1
""",
@ -394,9 +399,11 @@ def list_users(include_inactive: bool = False) -> List[Dict[str, Any]]:
sd.code AS service_department_code,
sd.phone AS service_department_phone,
sd.parent_id AS service_department_parent_id,
sd.region_id AS service_department_region_id
sd.region_id AS service_department_region_id,
r.name AS service_department_region_name
FROM auth_users au
LEFT JOIN service_departments sd ON sd.id = au.service_department_id
LEFT JOIN regions r ON r.id = sd.region_id
{where_clause}
ORDER BY au.created_at DESC
"""
@ -419,6 +426,7 @@ def create_user(
service_department_id: Optional[str] = None,
department_role: Optional[str] = None,
parent_department_id: Optional[str] = None,
service_department_region_id: Optional[str] = None,
service_department_phone: Optional[str] = None,
) -> Dict[str, Any]:
username_clean = (username or "").strip().lower()
@ -436,6 +444,7 @@ def create_user(
code=dept_code,
phone=(service_department_phone or "").strip() or None,
parent_id=(parent_department_id or "").strip() or None,
region_id=(service_department_region_id or "").strip() or None,
)
dept_token = created.get("id")
except Exception as exc:

View File

@ -1185,6 +1185,7 @@
<th>用户名</th>
<th>显示名</th>
<th>部门</th>
<th>区域</th>
<th>角色</th>
<th>创建时间</th>
<th>操作</th>
@ -1214,6 +1215,11 @@
<label>单位电话(可选)
<input type="text" name="department_phone" placeholder="用于自动创建的单位联系电话">
</label>
<label>所属区域(可选)
<select id="userCreateRegion" class="form-select" name="region_id">
<option value="">不选择区域(默认继承上级区域)</option>
</select>
</label>
<label>上级单位(可选,不选则创建顶级市级单位)
<select id="userCreateParent" class="form-select">
<option value="">不选择上级(顶级单位)</option>
@ -1400,6 +1406,7 @@ const rootDeptsEl = document.getElementById('rootDepts');
let state = {
users: [],
departments: [],
regions: [],
themes: [],
templateMeta: {},
userFilter: {
@ -1482,7 +1489,17 @@ async function loadCurrentUser() {
document.getElementById('currentUserName').textContent = user.display_name || user.username || '未知管理员';
document.getElementById('currentUserRole').textContent = user.role || 'admin';
const dept = user.department;
document.getElementById('currentUserDept').textContent = dept ? `${dept.name || ''}${dept.code ? ' · ' + dept.code : ''}` : '未绑定部门';
const deptPieces = [];
if (dept) {
if (dept.name) deptPieces.push(dept.name);
if (dept.code) deptPieces.push(dept.code);
if (dept.region_name) {
deptPieces.push(dept.region_name);
} else if (dept.region_id) {
deptPieces.push(dept.region_id);
}
}
document.getElementById('currentUserDept').textContent = dept ? (deptPieces.join(' · ') || '未绑定部门') : '未绑定部门';
} catch (err) {
console.error('认证失败:', err);
// 认证失败时重定向到登录页面
@ -1502,6 +1519,7 @@ function renderDepartmentOptions() {
deptParentSelect.innerHTML = parentOptions.join('');
}
renderUserCreateParentSelector();
renderUserCreateRegionSelector();
if (userEditDept) {
userEditDept.innerHTML = serviceOptions.join('');
}
@ -1520,6 +1538,37 @@ function renderUserCreateParentSelector() {
parentSelect.innerHTML = options.join('');
}
function buildRegionOptions(selectedId = '', includeBlank = true) {
const options = [];
if (includeBlank) {
options.push('<option value="">不选择区域(继承上级或稍后再设)</option>');
}
let hasSelected = false;
(state.regions || []).forEach(region => {
const value = region.id || '';
const name = region.name || value || '未命名区域';
const selected = String(selectedId || '') === String(value) ? 'selected' : '';
if (selected) {
hasSelected = true;
}
options.push(`<option value="${value}" ${selected}>${name}</option>`);
});
if (selectedId && !hasSelected && !options.find(opt => opt.includes(`value="${selectedId}"`))) {
options.push(`<option value="${selectedId}" selected>${selectedId}</option>`);
}
if (!options.length) {
options.push('<option value="">暂无区域数据</option>');
}
return options.join('');
}
function renderUserCreateRegionSelector() {
const select = document.getElementById('userCreateRegion');
if (!select) return;
const current = select.value;
select.innerHTML = buildRegionOptions(current, true);
}
function renderRoleOptions() {
const roles = new Set(DEFAULT_ROLE_ORDER);
state.users.forEach(user => {
@ -1548,7 +1597,8 @@ function applyUserFilters(users) {
const roleFilter = state.userFilter.role || '';
return users.filter(user => {
const deptName = user.department ? (user.department.name || '') : '';
const text = `${user.username || ''} ${user.display_name || ''} ${deptName}`.toLowerCase();
const regionName = user.department ? (user.department.region_name || user.department.region_id || '') : '';
const text = `${user.username || ''} ${user.display_name || ''} ${deptName} ${regionName}`.toLowerCase();
const matchKeyword = !keyword || text.includes(keyword);
const matchRole = !roleFilter || user.role === roleFilter;
return matchKeyword && matchRole;
@ -1559,16 +1609,18 @@ function renderUserTable() {
if (!userTableBody) return;
const filtered = applyUserFilters(state.users);
if (!filtered.length) {
userTableBody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:#6b7280;padding:14px 0;">暂无用户,点击右上角“创建账号”开始添加</td></tr>`;
userTableBody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:#6b7280;padding:14px 0;">暂无用户,点击右上角“创建账号”开始添加</td></tr>`;
return;
}
userTableBody.innerHTML = filtered.map(user => {
const dept = user.department ? user.department.name : '—';
const region = user.department ? (user.department.region_name || user.department.region_id || '—') : '—';
const isActive = activeUserId && String(user.id) === String(activeUserId);
return `<tr data-id="${user.id}" class="${isActive ? 'active-row' : ''}">
<td>${user.username}</td>
<td>${user.display_name || '—'}</td>
<td>${dept}</td>
<td>${region}</td>
<td><span class="pill">${getRoleLabel(user.role)}</span></td>
<td>${formatDate(user.created_at)}</td>
<td>
@ -1634,6 +1686,12 @@ async function refreshUsers(options = {}) {
}
}
async function refreshRegions() {
const data = await fetchJSON(`${API_BASE}/admin/regions`, {method: 'GET'});
state.regions = data.data.regions || [];
renderUserCreateRegionSelector();
}
async function refreshDepartments() {
const data = await fetchJSON(`${API_BASE}/admin/service-departments`, {method: 'GET'});
state.departments = data.data.departments || [];
@ -1676,12 +1734,14 @@ function fillUserDrawer(user) {
if (userSummaryBox) {
const dept = user.department ? `${user.department.name || ''}${user.department.code ? ' · ' + user.department.code : ''}` : '未绑定';
const deptPhone = user.department ? (user.department.phone || '—') : '—';
const region = user.department ? (user.department.region_name || user.department.region_id || '—') : '—';
userSummaryBox.innerHTML = `
<div class="summary-grid">
<div><div class="label">登录账号</div><div class="value">${user.username || '—'}</div></div>
<div><div class="label">显示名</div><div class="value">${user.display_name || '—'}</div></div>
<div><div class="label">角色</div><div class="value"><span class="pill">${getRoleLabel(user.role)}</span></div></div>
<div><div class="label">绑定部门</div><div class="value">${dept}</div></div>
<div><div class="label">所属区域</div><div class="value">${region}</div></div>
<div><div class="label">单位电话</div><div class="value">${deptPhone}</div></div>
<div><div class="label">创建时间</div><div class="value">${formatDate(user.created_at)}</div></div>
</div>
@ -1778,13 +1838,25 @@ if (userCreateForm) {
evt.preventDefault();
const form = evt.target;
const parentSelect = document.getElementById('userCreateParent');
const regionSelect = document.getElementById('userCreateRegion');
const parentDepartmentId = parentSelect ? (parentSelect.value || '') : '';
let regionId = regionSelect ? regionSelect.value : '';
if (!regionId && parentDepartmentId) {
const parentDept = state.departments.find(dept => String(dept.id) === String(parentDepartmentId));
if (parentDept) {
regionId = parentDept.region_id || '';
}
}
if (!regionId && !parentDepartmentId) {
showMessage('请选择所属区域,或指定上级单位继承区域', 'error');
return;
}
const payload = {
username: form.username.value.trim(),
password: form.password.value.trim(),
display_name: form.display_name.value.trim(),
parent_department_id: parentDepartmentId || null,
region_id: regionId || null,
department_phone: (form.department_phone.value || '').trim()
};
try {
@ -1799,6 +1871,9 @@ if (userCreateForm) {
if (parentSelect) {
parentSelect.value = '';
}
if (regionSelect) {
regionSelect.value = '';
}
form.department_phone.value = '';
switchUserSubtab('user-list-panel');
await refreshDepartments();
@ -1807,6 +1882,18 @@ if (userCreateForm) {
showMessage(err.message, 'error');
}
});
const parentSelect = document.getElementById('userCreateParent');
const regionSelect = document.getElementById('userCreateRegion');
if (parentSelect && regionSelect) {
parentSelect.addEventListener('change', () => {
if (regionSelect.value) {
return;
}
const parentDept = state.departments.find(dept => String(dept.id) === String(parentSelect.value || ''));
regionSelect.value = parentDept ? (parentDept.region_id || '') : '';
});
}
}
if (userEditForm) {
@ -2084,6 +2171,7 @@ function flattenTree(nodes, level = 0, result = [], parentId = null) {
name: node.name || '未知部门',
code: node.code || '',
region_name: node.region_name || '',
region_id: node.region_id || '',
level,
node
};
@ -2556,6 +2644,7 @@ document.getElementById('downloadTemplateBtn').addEventListener('click', () => {
async function bootstrap() {
await loadCurrentUser();
await Promise.all([
refreshRegions(),
refreshUsers(),
refreshDepartments(),
refreshThemes(),
@ -2634,6 +2723,7 @@ function showAddChildModal(parentNode) {
name: formData.get('name'),
code: formData.get('code'),
phone: formData.get('phone'),
region_id: formData.get('region_id'),
parent_id: parentNode.id,
description: formData.get('description'),
grade: autoGrade // 自动计算,无需手动选择
@ -2662,6 +2752,12 @@ function showAddChildModal(parentNode) {
<label>联系电话
<input type="text" name="phone" placeholder="座机或手机号">
</label>
<label>所属区域
<select name="region_id">
${buildRegionOptions(parentNode.region_id || '', true)}
</select>
<small style="color: #6b7280; font-size: 12px;">不选择则默认继承上级区域</small>
</label>
<label>备注
<textarea name="description" rows="2" placeholder="可填写简要职责"></textarea>
</label>
@ -2676,6 +2772,7 @@ function showEditModal(nodeData) {
return {
name: formData.get('name'),
phone: formData.get('phone'),
region_id: formData.get('region_id'),
description: formData.get('description')
// 编辑时不修改gradegrade根据层级自动计算
};
@ -2702,6 +2799,11 @@ function showEditModal(nodeData) {
<label>联系电话
<input type="text" name="phone" value="${nodeData.phone || ''}">
</label>
<label>所属区域
<select name="region_id">
${buildRegionOptions(nodeData.region_id || '', true)}
</select>
</label>
<label>备注
<textarea name="description" rows="2">${nodeData.description || ''}</textarea>
</label>