# sunderer_app/api/custom_user_signup.py import frappe from frappe import _ # 虽然这里不直接用 _(),但保持导入 from frappe.utils import validate_email_address, random_string, get_url, generate_hash import json # 导入 json 模块 @frappe.whitelist(allow_guest=True) def custom_sign_up(email, full_name, password=None, send_email=True, **kwargs): """ 自定义用户注册 API,模仿 Frappe 的 sign_up 但发送自定义邮件链接 Args: email (str): 用户邮箱 full_name (str): 用户全名 password (str, optional): 用户密码 (如果需要立即设置) send_email (bool, optional): 是否发送激活邮件. Defaults to True. **kwargs: 其他可能传递给 User DocType 的字段 Returns: dict: 包含操作结果的字典 """ try: # --- 1. 验证输入 --- email = validate_email_address(email, throw=True) # 验证邮箱格式 if frappe.db.exists("User", {"email": email}): # 不再使用 frappe.throw,而是直接返回错误信息 return {"status": "error", "message": _("该邮箱地址已被注册。")} if not full_name: return {"status": "error", "message": _("请输入全名。")} # --- 2. 创建 User 文档 --- user = frappe.get_doc({ "doctype": "User", "email": email, "first_name": full_name.split(" ")[0], # 简单拆分姓 "last_name": " ".join(full_name.split(" ")[1:]) if len(full_name.split(" ")) > 1 else "", # 其余部分为名 "enabled": 1 if password else 0, # 如果提供了密码,可以立即启用;否则等待激活 "new_password": password, # 如果提供了密码,设置它 "send_welcome_email": 0, # 我们自己发送邮件 "user_type": "Website User", # 通常注册的是网站用户 # **kwargs: 添加其他通过 kwargs 传递的字段 **kwargs }) # --- 3. 插入用户文档 --- user.insert(ignore_permissions=True) # --- 4. 生成激活密钥 --- # 如果没有提供密码,或者总是需要激活邮件,则生成密钥 if not password: # Use frappe.generate_hash instead of get_password_reset_key reset_key = frappe.generate_hash(length=100) # Generate a secure key else: reset_key = None # 如果密码已设置,可能不需要密钥,或用于其他目的 # --- 5. 保存用户文档 (如果需要更新密钥) --- if reset_key: user.reload() # 重新加载文档以确保它是最新的 user.reset_password_key = reset_key user.save(ignore_permissions=True) # --- 6. 发送自定义激活邮件 --- if send_email and reset_key: send_custom_activation_email(user.email, reset_key) # --- 7. 返回结果 --- if password: return {"status": "success", "message": _("用户注册成功,您现在可以登录。")} else: return {"status": "success", "message": _("激活邮件已发送,请查收邮箱。")} except frappe.DuplicateEntryError: # 这个异常可能在 user.insert() 时由数据库唯一性约束触发 return {"status": "error", "message": _("该邮箱地址已被注册。")} except Exception as e: # 捕获所有其他意外错误 frappe.log_error(f"注册过程中发生未知错误,邮箱: {email}, 错误: {str(e)}") return {"status": "error", "message": _("注册过程中发生未知错误,请稍后再试。")} @frappe.whitelist(allow_guest=True) def send_custom_activation_email(user_email, reset_key): """ 发送包含自定义激活链接的邮件 """ # 构造自定义激活 URL activation_url = f"{get_url()}/activate-account?key={reset_key}" # Use get_url helper # 使用中文主题和内容 subject = _("决裂者终端 - 激活您的账户") # 或者 _("决裂者终端 - 设置密码") message = f"""

您好!

请点击下方链接设置密码并激活您的账户:

{activation_url}

如果您未发起此请求,请忽略此邮件。

""" frappe.sendmail( recipients=[user_email], subject=subject, message=message, delayed=False # Send immediately ) @frappe.whitelist(allow_guest=True) def trigger_account_setup_email(email): try: # --- 1. 验证邮箱格式 (可选,但推荐) --- import re pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' if not re.match(pattern, email): return {"exc": "请输入有效的邮箱地址!"} # --- 2. 检查邮箱是否已存在于系统中 --- if frappe.db.exists("User", {"email": email}): return {"exc": "该邮箱地址已被注册。"} # --- 3. 生成唯一的、有时效性的设置密钥 --- setup_key = frappe.generate_hash(length=100) # 生成一个足够长的随机密钥 # --- 4. 创建初始的 User 记录 (禁用) --- # 由于没有全名,我们暂时使用邮箱前缀作为名字,或者可以要求前端提供 username_parts = email.split('@') first_name = username_parts[0] # 使用邮箱前缀作为名字 user_doc = frappe.get_doc({ "doctype": "User", "email": email, "first_name": first_name, "last_name": "", # 可以留空或根据需要填充 "enabled": 0, # 初始状态为禁用 "send_welcome_email": 0, # 我们自己发送邮件 "user_type": "Website User", # 可以根据需要添加其他默认字段 }) user_doc.insert(ignore_permissions=True) # 插入用户,此时是禁用的 print(f"Created initial disabled User: {user_doc.name}") # 调试日志 # --- 5. 创建 Signup Key DocType 记录 --- signup_key_doc = frappe.new_doc("Signup Key") signup_key_doc.key = setup_key # 使用生成的 setup_key 作为 key 字段 signup_key_doc.email = email # 存储关联的邮箱 signup_key_doc.used = 0 # 标记为未使用 # 如果 Signup Key DocType 有 expires_on 字段,可以设置过期时间 # signup_key_doc.expires_on = add_to_date(now_datetime(), hours=1) # 1小时后过期 signup_key_doc.insert(ignore_permissions=True) print(f"Created Signup Key: {signup_key_doc.name} for email: {email}") # 调试日志 # --- 6. 构造 IncreaseAcc 链接 --- site_url = get_url() setup_url = f"{site_url}/IncreaseAcc?key={setup_key}" # --- 7. 发送包含链接的邮件 --- send_setup_link_email(email, setup_url) # --- 8. 返回成功信息 --- return {"message": "注册链接已发送至您的邮箱,请查收。"} except frappe.DuplicateEntryError as e: # 如果 User 或 Signup Key 插入时违反唯一性约束 frappe.log_error(f"Trigger account setup failed due to duplicate entry, email: {email}, error: {str(e)}") return {"exc": "用户或密钥已存在,请稍后再试。"} except Exception as e: # 捕获所有其他意外错误 frappe.log_error(f"触发账户设置邮件失败,邮箱: {email}, 错误: {str(e)}") import traceback frappe.log_error(f"触发账户设置邮件错误堆栈:\n{traceback.format_exc()}") # 如果 User 创建失败,尝试回滚 Signup Key (可选) # frappe.delete_doc_if_exists("Signup Key", setup_key_name, ignore_permissions=True) return {"exc": f"发送失败: {str(e)}"} @frappe.whitelist(allow_guest=True) def send_setup_link_email(user_email, setup_url): """ 发送包含账户设置链接的邮件 """ # 使用中文主题和内容 subject = "决裂者终端 - 完成账户注册" message = f"""

决裂者终端

您好!

请点击下方链接完成您的账户注册:

完成注册

此链接将在 1 小时内有效。

如果您未发起此请求,请忽略此邮件。


© 2026 Sunderer Games. All rights reserved.

""" frappe.sendmail( recipients=[user_email], subject=subject, message=message, delayed=False # 立即发送 ) @frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True) def complete_account(key, password): print(f"[DEBUG] 开始执行 complete_account,传入的 key: {key[:10]}...") try: signup_key_doc_list = frappe.get_all("Signup Key", fields=["name", "email"], filters={"key": key, "used": 0}) print(f"[DEBUG] 查询到的 signup_key_doc_list: {signup_key_doc_list}") if not signup_key_doc_list: print("[DEBUG] 未找到有效的未使用密钥,抛出错误。") frappe.throw("无效或已过期的密钥。", frappe.ValidationError) return signup_key_name = signup_key_doc_list[0].name email = signup_key_doc_list[0].email print(f"[DEBUG] 获取到的邮箱: {email}") # --- 获取用户文档 --- try: user_doc = frappe.get_doc("User", email) print(f"[DEBUG] 找到用户文档: {user_doc.name}, 启用状态: {user_doc.enabled}") except frappe.DoesNotExistError: print(f"[ERROR] 用户 {email} 不存在。") frappe.throw(f"用户 {email} 不存在。") return # --- 新增:手动进行密码强度校验 --- # 为密码强度检查准备用户信息 user_info = { "email": user_doc.email or "", "first_name": user_doc.first_name or "", "last_name": user_doc.last_name or "", "username": user_doc.name or "" # 如果 User 文档有 username 字段,也可以包含 } # 调用 Frappe 的密码强度检查函数 # 注意:这个函数名可能因版本而异,但通常在 frappe.utils.password_strength 中 # 我们会尝试不同的函数名 try: from frappe.utils.password_strength import get_password_strength strength_result = get_password_strength(password, user_info) except ImportError: # 如果无法导入 get_password_strength,尝试其他方式 # 这里我们假设它在 frappe.utils 中,或者使用一个简单的自定义检查 strength_result = {"score": 0, "feedback": {"warning": "", "suggestions": ["密码至少需要8位字符,包含大小写字母和数字"]}} # 或者,您可以在这里实现更复杂的自定义强度检查逻辑 score = strength_result.get('score', 0) feedback = strength_result.get('feedback', {}) # 定义最低分数要求(Frappe 默认是2) min_score = 2 if score < min_score: suggestions = feedback.get('suggestions', []) warning = feedback.get('warning', "") error_message_parts = ["密码强度不足,请设置一个更强的密码。"] if warning: error_message_parts.append(f"警告: {warning}") if suggestions: error_message_parts.append(f"建议: {', '.join(suggestions)}") # 使用 frappe.throw 返回结构化的错误信息,便于前端解析 frappe.throw({ "message": " ".join(error_message_parts), "type": "password_strength", "details": { "score": score, "min_score": min_score, "suggestions": suggestions, "warning": warning } }) # --- 新增结束:手动进行密码强度校验 --- # --- 关键部分:更新密码和角色 --- try: user_doc.new_password = password user_doc.enabled = 1 # 确保用户处于启用状态 # --- 添加或修改角色 --- default_roles = ["Sunderer User"] # 根据你的需求调整 for role_name in default_roles: if frappe.db.exists("Role", role_name): if not any(d.role == role_name for d in user_doc.roles): user_doc.append("roles", {"role": role_name}) print(f"[DEBUG] 已将角色 '{role_name}' 添加到用户 {email}。") else: print(f"[WARNING] 角色 '{role_name}' 不存在。跳过。") # --- 角色添加结束 --- # 在手动检查通过后,再执行 save(),此时应该不会再因为密码强度问题失败 user_doc.save(ignore_permissions=True) print(f"[DEBUG] 成功更新用户 {email} 的密码、角色和启用状态。") except Exception as pwd_error: frappe.log_error(f"在完成账户 {email} 时更新用户详细信息(密码/角色)失败: {str(pwd_error)}") print(f"[ERROR] 更新用户 {email} 详细信息失败: {str(pwd_error)}") frappe.throw(f"设置用户账户失败: {str(pwd_error)}") return # --- 仅当用户信息更新成功后,才将密钥标记为已使用 --- try: frappe.db.set_value("Signup Key", signup_key_name, "used", 1) print(f"[DEBUG] 已将 Signup Key {signup_key_name} 标记为已使用。") except Exception as db_error: frappe.log_error(f"用户更新后,将 Signup Key {signup_key_name} 标记为已使用失败: {str(db_error)}") print(f"[ERROR] 将 Signup Key 标记为已使用失败: {str(db_error)}") frappe.msgprint("密码已设置,但请就确认链接状态联系技术支持。") frappe.db.commit() print(f"[DEBUG] 已提交 key: {key[:10]}... 的事务。") return {"message": "账户已激活,密码设置成功!"} except frappe.AuthenticationError as e: frappe.log_error(f"complete_account 中发生身份验证错误: {str(e)}") print(f"[ERROR] 身份验证错误: {str(e)}") frappe.throw(f"身份验证错误: {str(e)}") except Exception as e: frappe.log_error(f"激活账户时发生错误,key {key[:10]}...: {str(e)}") print(f"[ERROR] 激活账户时发生错误,key {key[:10]}...: {str(e)}") frappe.throw("激活您的账户时发生错误。请稍后重试或联系技术支持。")