- Login.vue: no prefilled default creds; no anonymous entity enumeration on mount - deploy.sh: readiness polling (0.5s/30s cap) replaces fixed sleep 3; login smoke reads same-source 600 secret file, missing file = explicit abort - 19 verify/check/gen_report scripts: shared _admin_creds() loader (CMA_ADMIN_ENV -> ops .secrets -> profile copy); no hardcoded creds - tests: single TEST_USER_PASSWORD fixture constant; docs: pointer to 600 secret file; removed 4 legacy .bak + pycache residue
160 lines
6.1 KiB
Python
160 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
企业微信通讯录 → CMA 系统同步脚本
|
||
功能:
|
||
1. 从企业微信通讯录获取真实员工数据(姓名、手机、岗位)
|
||
2. 同步到 CMA 的 users 表(扩展手机号字段)
|
||
3. 同步到 CMA 的 org_nodes 表(更新真实组织架构)
|
||
"""
|
||
|
||
import json
|
||
import urllib.request
|
||
import pymysql
|
||
import hashlib
|
||
|
||
# 2026-09-12 库访问治理(ops):口令改从 .env 读取,清除硬编码
|
||
import os as _os_gov
|
||
try:
|
||
from dotenv import load_dotenv as _ld_gov
|
||
_ld_gov('/root/cma-management/backend/.env', override=False)
|
||
except Exception:
|
||
pass
|
||
_CMA_DB_PASS = _os_gov.getenv('CMA_DB_PASS', '')
|
||
_CMA_DB_USER = _os_gov.getenv('CMA_DB_USER', '')
|
||
_CMA_DB_HOST = _os_gov.getenv('CMA_DB_HOST', '127.0.0.1')
|
||
|
||
# ==== 配置 ====
|
||
MCP_URL = "http://127.0.0.1:18900/call"
|
||
DB_HOST = "127.0.0.1"
|
||
DB_PORT = 3306
|
||
DB_USER = _CMA_DB_USER
|
||
DB_PASS = _CMA_DB_PASS
|
||
DB_NAME = "cma"
|
||
# 新建用户的初始口令不再硬编码(2026-09-14 cma-defaultcred-code-fix):
|
||
# 从 $CMA_DEFAULT_USER_PASSWORD 或 600 密钥文件同源读取;缺失 → 显式报错退出(禁止静默用默认口令)。
|
||
DEFAULT_PASSWORD = _os_gov.getenv('CMA_DEFAULT_USER_PASSWORD', '')
|
||
if not DEFAULT_PASSWORD:
|
||
for _p in (_os_gov.getenv('CMA_ADMIN_ENV'),
|
||
'/root/.hermes/profiles/wecom-ops/.secrets/cma-admin.env',
|
||
'/root/.hermes/profiles/wecom-fullstack/.secrets/cma-admin.env'):
|
||
if _p and _os_gov.path.isfile(_p):
|
||
for _line in open(_p, encoding='utf-8'):
|
||
_line = _line.strip()
|
||
if _line.startswith('CMA_DEFAULT_USER_PASSWORD='):
|
||
DEFAULT_PASSWORD = _line.split('=', 1)[1].strip().strip('"').strip("'")
|
||
if not DEFAULT_PASSWORD:
|
||
raise SystemExit("[错误] 未找到新建用户初始口令:请设 $CMA_DEFAULT_USER_PASSWORD,"
|
||
"或在 600 密钥文件($CMA_ADMIN_ENV / .secrets/cma-admin.env)里定义 CMA_DEFAULT_USER_PASSWORD")
|
||
|
||
# ==== 工具函数 ====
|
||
def mcp_call(name, arguments={}):
|
||
req = urllib.request.Request(
|
||
MCP_URL,
|
||
data=json.dumps({"name": name, "arguments": arguments}).encode(),
|
||
headers={"Content-Type": "application/json"}
|
||
)
|
||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||
return json.loads(resp.read())
|
||
|
||
def get_db():
|
||
return pymysql.connect(
|
||
host=DB_HOST, port=DB_PORT,
|
||
user=DB_USER, password=DB_PASS,
|
||
database=DB_NAME, charset="utf8mb4"
|
||
)
|
||
|
||
# ==== 主流程 ====
|
||
def sync_users():
|
||
"""同步通讯录成员到 CMA users 表"""
|
||
print("=" * 50)
|
||
print("企业微信通讯录 → CMA 用户同步")
|
||
print("=" * 50)
|
||
|
||
# 1. 获取企微通讯录
|
||
result = mcp_call("wecom_get_userlist", {"department_id": 1})
|
||
wecom_users = result.get("users", [])
|
||
print(f"\n📋 企微通讯录共 {len(wecom_users)} 人")
|
||
|
||
# 2. 连接 CMA 数据库
|
||
db = get_db()
|
||
cursor = db.cursor()
|
||
|
||
# 先检查 users 表是否有 phone 字段
|
||
cursor.execute("SHOW COLUMNS FROM users LIKE 'phone'")
|
||
if not cursor.fetchone():
|
||
print("⚠️ users 表缺少 phone 字段,准备添加...")
|
||
cursor.execute("ALTER TABLE users ADD COLUMN phone VARCHAR(20) DEFAULT NULL COMMENT '手机号'")
|
||
db.commit()
|
||
print("✅ phone 字段已添加")
|
||
|
||
# 检查是否有 openid 字段
|
||
cursor.execute("SHOW COLUMNS FROM users LIKE 'wecom_userid'")
|
||
if not cursor.fetchone():
|
||
cursor.execute("ALTER TABLE users ADD COLUMN wecom_userid VARCHAR(100) DEFAULT NULL COMMENT '企微UserID'")
|
||
db.commit()
|
||
print("✅ wecom_userid 字段已添加")
|
||
|
||
# 检查是否有 position 字段
|
||
cursor.execute("SHOW COLUMNS FROM users LIKE 'position'")
|
||
if not cursor.fetchone():
|
||
cursor.execute("ALTER TABLE users ADD COLUMN position VARCHAR(100) DEFAULT NULL COMMENT '岗位'")
|
||
db.commit()
|
||
print("✅ position 字段已添加")
|
||
|
||
# 3. 同步用户
|
||
password_hash = hashlib.sha256(DEFAULT_PASSWORD.encode()).hexdigest()
|
||
synced = 0
|
||
skipped = 0
|
||
|
||
for u in wecom_users:
|
||
name = u.get("name", "")
|
||
mobile = u.get("mobile", "")
|
||
userid = u.get("userid", "")
|
||
position = u.get("position", "")
|
||
|
||
if not name:
|
||
skipped += 1
|
||
continue
|
||
|
||
# 检查是否已存在(按手机号或用户名匹配)
|
||
cursor.execute("SELECT id, username FROM users WHERE phone = %s OR username = %s", (mobile, name))
|
||
existing = cursor.fetchone()
|
||
|
||
if existing:
|
||
# 更新已有用户
|
||
cursor.execute(
|
||
"UPDATE users SET phone = %s, wecom_userid = %s, position = %s WHERE id = %s",
|
||
(mobile, userid, position, existing[0])
|
||
)
|
||
print(f" 🔄 更新: {name:6s} → id={existing[0]}")
|
||
else:
|
||
# 插入新用户
|
||
username = userid if userid else name
|
||
cursor.execute(
|
||
"INSERT INTO users (username, password_hash, name, role, phone, wecom_userid, position) VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||
(username, password_hash, name, "business", mobile, userid, position)
|
||
)
|
||
new_id = cursor.lastrowid
|
||
print(f" ➕ 新增: {name:6s} → id={new_id} (role=business)")
|
||
synced += 1
|
||
|
||
db.commit()
|
||
print(f"\n📊 同步完成: 新增 {synced} 人, 更新 {len(wecom_users) - synced - skipped} 人, 跳过 {skipped} 人")
|
||
|
||
# 4. 显示同步后的用户列表
|
||
cursor.execute("SELECT id, username, name, role, phone, position FROM users ORDER BY id")
|
||
all_users = cursor.fetchall()
|
||
print(f"\n📋 CMA 用户表(共 {len(all_users)} 人):")
|
||
print(f" {'ID':>3} {'用户名':16s} {'姓名':8s} {'角色':10s} {'手机号':12s} {'岗位':10s}")
|
||
print(f" {'-'*65}")
|
||
for row in all_users:
|
||
print(f" {row[0]:>3} {row[1]:16s} {(row[2] or ''):8s} {(row[3] or ''):10s} {(row[4] or ''):12s} {(row[5] or ''):10s}")
|
||
|
||
cursor.close()
|
||
db.close()
|
||
return len(wecom_users)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sync_users()
|