feat: 固定个人与企业认证流程

This commit is contained in:
2026-07-31 10:56:59 +08:00
parent b4364e63ec
commit ec63b446e8
8 changed files with 139 additions and 111 deletions
+10 -4
View File
@@ -615,15 +615,21 @@ class SchemaUsers {
['key' => 'display', 'title' => '公开展示', 'fields' => [
['key' => 'user_verify_enabled', 'label' => '启用身份认证', 'type' => 'switch', 'default' => true],
['key' => 'user_verify_public_display', 'label' => '公开页显示认证徽标', 'type' => 'switch', 'default' => true],
['key' => 'user_verify_badge_text', 'label' => '认证徽标文案', 'type' => 'input', 'default' => '已认证'],
['key' => 'user_verify_personal_badge_text', 'label' => '个人认证徽标文案', 'type' => 'input', 'default' => '个人认证'],
['key' => 'user_verify_enterprise_badge_text', 'label' => '企业认证徽标文案', 'type' => 'input', 'default' => '企业认证'],
]],
['key' => 'workflow', 'title' => '申请类型', 'fields' => [
['key' => 'user_verify_apply_option', 'label' => '申请类型选项', 'type' => 'textarea', 'default' => "real_name|实名认证\norganization|企业认证", 'desc' => '每行:slug|显示名称'],
['key' => 'workflow', 'title' => '认证方式', 'fields' => [
['type' => 'alert', 'content' => '认证方式固定为个人认证和企业认证,申请人不可自行新增或修改类型。'],
]],
['key' => 'apply', 'title' => '申请设置', 'fields' => [
['key' => 'user_verify_allow_apply', 'label' => '允许用户前台申请', 'type' => 'switch', 'default' => true],
['key' => 'user_verify_disable_desc', 'label' => '停用申请说明', 'type' => 'textarea', 'default' => "由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员"],
['key' => 'user_verify_limit_level', 'label' => '申请要求等级', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 99, 'desc' => '0 表示不限制;依赖用户等级'],
['key' => 'user_verify_personal_fee', 'label' => '个人认证每次扣余额(元)', 'type' => 'number', 'default' => 0, 'min' => 0, 'step' => 0.01],
['key' => 'user_verify_personal_free_applications', 'label' => '个人认证赠送初始次数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'desc' => '每位用户前 N 次个人认证申请免费,之后按单次费用扣余额。'],
['key' => 'user_verify_personal_require_real_name', 'label' => '个人认证强制填写真实姓名', 'type' => 'switch', 'default' => false],
['key' => 'user_verify_enterprise_fee', 'label' => '企业认证每次扣余额(元)', 'type' => 'number', 'default' => 0, 'min' => 0, 'step' => 0.01],
['key' => 'user_verify_enterprise_free_applications', 'label' => '企业认证赠送初始次数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'desc' => '每位用户前 N 次企业认证申请免费,之后按单次费用扣余额。'],
['key' => 'user_verify_enterprise_require_real_name', 'label' => '企业认证强制填写真实姓名', 'type' => 'switch', 'default' => false],
['key' => 'user_verify_desc', 'label' => '申请介绍', 'type' => 'textarea', 'default' => '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。'],
]],
],
+23 -9
View File
@@ -142,7 +142,7 @@ class UserRuntime {
* User identity, level, medal, and report governance settings.
*
* @return array{
* verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,allow_apply:bool,disable_desc:string,limit_level:int,desc:string,apply_option:string},
* verify:array{enabled:bool,public_display:bool,allow_apply:bool,disable_desc:string,desc:string,types:array<string,array{slug:string,label:string,badge_text:string,fee:float,free_applications:int,require_real_name:bool}>},
* level:array{enabled:bool,public_display:bool,source:string,label_prefix:string,thresholds:array<int,int>},
* medal:array{enabled:bool,public_display:bool,limit:int,categories:array<int,array{cat_name:string,items:array<int,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int}>}>,items_by_slug:array<string,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int,cat_name:string}>},
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,icon_url?:string}>},
@@ -151,21 +151,35 @@ class UserRuntime {
*/
public static function get_user_identity_settings(): array {
$settings = Settings::get_all();
$verify_badge_text = trim((string) ($settings['user_verify_badge_text'] ?? ''));
$ban_notice_text = trim((string) ($settings['user_report_ban_notice_text'] ?? ''));
$verify_types = [
'personal' => [
'slug' => 'personal',
'label' => __('个人认证', 'aksrt'),
'badge_text' => trim((string) ($settings['user_verify_personal_badge_text'] ?? '')) ?: __('个人认证', 'aksrt'),
'fee' => max(0, round((float) ($settings['user_verify_personal_fee'] ?? 0), 2)),
'free_applications' => max(0, (int) ($settings['user_verify_personal_free_applications'] ?? 0)),
'require_real_name' => !empty($settings['user_verify_personal_require_real_name']),
],
'enterprise' => [
'slug' => 'enterprise',
'label' => __('企业认证', 'aksrt'),
'badge_text' => trim((string) ($settings['user_verify_enterprise_badge_text'] ?? '')) ?: __('企业认证', 'aksrt'),
'fee' => max(0, round((float) ($settings['user_verify_enterprise_fee'] ?? 0), 2)),
'free_applications' => max(0, (int) ($settings['user_verify_enterprise_free_applications'] ?? 0)),
'require_real_name' => !empty($settings['user_verify_enterprise_require_real_name']),
],
];
$medal = self::parse_medal_catalog($settings);
return [
'verify' => [
'enabled' => !isset($settings['user_verify_enabled']) || !empty($settings['user_verify_enabled']),
'public_display' => !isset($settings['user_verify_public_display']) || !empty($settings['user_verify_public_display']),
'badge_text' => $verify_badge_text !== '' ? $verify_badge_text : __('已认证', 'aksrt'),
'accept_email_verified' => false,
'allow_apply' => !isset($settings['user_verify_allow_apply']) || !empty($settings['user_verify_allow_apply']),
'disable_desc' => (string) ($settings['user_verify_disable_desc'] ?? ''),
'limit_level' => max(0, (int) ($settings['user_verify_limit_level'] ?? 0)),
'desc' => (string) ($settings['user_verify_desc'] ?? ''),
'apply_option' => (string) ($settings['user_verify_apply_option'] ?? ''),
'allow_apply' => !isset($settings['user_verify_allow_apply']) || !empty($settings['user_verify_allow_apply']),
'disable_desc' => (string) ($settings['user_verify_disable_desc'] ?? ''),
'desc' => (string) ($settings['user_verify_desc'] ?? ''),
'types' => $verify_types,
],
'level' => [
'enabled' => !isset($settings['user_level_s']) || !empty($settings['user_level_s']),
+61 -57
View File
@@ -19,6 +19,7 @@ class AuthApply {
public const REVIEWED_AT_META = 'aksrt_user_verify_reviewed_at';
public const REVIEWER_META = 'aksrt_user_verify_reviewer';
public const HISTORY_META = 'aksrt_user_verify_history';
public const APPLICATION_COUNTS_META = 'aksrt_user_verify_application_counts';
private static $instance = null;
@@ -40,49 +41,30 @@ class AuthApply {
}
}
/**
* @return array{
* enabled:bool,allow_apply:bool,public_display:bool,badge_text:string,
* limit_level:int,desc:string,disable_desc:string,
* types:array<int,array{slug:string,label:string}>
* }
*/
/** @return array{enabled:bool,allow_apply:bool,public_display:bool,limit_level:int,desc:string,disable_desc:string,types:array<string,array{slug:string,label:string,badge_text:string,fee:float,free_applications:int,require_real_name:bool}>} */
public static function settings(): array {
$all = class_exists('\AKSRT\Settings\Settings')
? (array) \AKSRT\Settings\Settings::get_all()
: [];
$enabled = !isset($all['user_verify_enabled']) || !empty($all['user_verify_enabled']);
$types_raw = (string) ($all['user_verify_apply_option'] ?? "real_name|实名认证\norganization|企业认证");
$types = [];
foreach (preg_split('/\r\n|\r|\n/', $types_raw) as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
$parts = array_map('trim', explode('|', $line, 2));
$slug = sanitize_key($parts[0] ?? '');
$label = $parts[1] ?? $parts[0];
if ($slug === '') {
continue;
}
$types[] = ['slug' => $slug, 'label' => (string) $label];
}
if ($types === []) {
$types = [
['slug' => 'real_name', 'label' => __('实名认证', 'aksrt')],
['slug' => 'organization', 'label' => __('企业认证', 'aksrt')],
];
}
return [
'enabled' => $enabled,
'allow_apply' => !isset($all['user_verify_allow_apply']) || !empty($all['user_verify_allow_apply']),
'public_display' => !isset($all['user_verify_public_display']) || !empty($all['user_verify_public_display']),
'badge_text' => (string) ($all['user_verify_badge_text'] ?? __('已认证', 'aksrt')),
'limit_level' => max(0, (int) ($all['user_verify_limit_level'] ?? 0)),
'desc' => (string) ($all['user_verify_desc'] ?? ''),
'disable_desc' => (string) ($all['user_verify_disable_desc'] ?? ''),
'types' => $types,
];
$identity = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_user_identity_settings() : [];
return is_array($identity['verify'] ?? null) ? $identity['verify'] : [];
}
/** @return array<string,mixed> */
public static function type_settings(string $type): array {
$types = self::settings()['types'] ?? [];
return is_array($types[sanitize_key($type)] ?? null) ? $types[sanitize_key($type)] : [];
}
public static function application_count(int $user_id, string $type): int {
$counts = get_user_meta($user_id, self::APPLICATION_COUNTS_META, true);
$counts = is_array($counts) ? $counts : [];
return max(0, (int) ($counts[sanitize_key($type)] ?? 0));
}
private static function increment_application_count(int $user_id, string $type): void {
$counts = get_user_meta($user_id, self::APPLICATION_COUNTS_META, true);
$counts = is_array($counts) ? $counts : [];
$type = sanitize_key($type);
$counts[$type] = max(0, (int) ($counts[$type] ?? 0)) + 1;
update_user_meta($user_id, self::APPLICATION_COUNTS_META, $counts);
}
public static function get_status(int $user_id): string {
@@ -257,6 +239,7 @@ class AuthApply {
public static function application_view(array $application, int $owner_id = 0, bool $admin_context = false): array {
return [
'name' => (string) ($application['name'] ?? ''),
'real_name' => (string) ($application['real_name'] ?? ''),
'desc' => (string) ($application['desc'] ?? ''),
'msg' => (string) ($application['msg'] ?? ''),
'type' => sanitize_key((string) ($application['type'] ?? '')),
@@ -409,33 +392,27 @@ class AuthApply {
if (self::get_status($user_id) === 'pending') {
wp_send_json_error(['message' => __('您的申请正在审核中', 'aksrt')], 409);
}
if ($s['limit_level'] > 0 && class_exists('\AKSRT\User\Level')) {
$lv = (int) Level::get_level($user_id);
if ($lv < $s['limit_level']) {
wp_send_json_error([
'message' => sprintf(
/* translators: %d: required level */
__('等级不足,需达到 LV.%d 后申请', 'aksrt'),
$s['limit_level']
),
], 403);
}
}
$name = isset($_POST['name']) ? sanitize_text_field(wp_unslash((string) $_POST['name'])) : '';
$real_name = isset($_POST['real_name']) ? sanitize_text_field(wp_unslash((string) $_POST['real_name'])) : '';
$desc = isset($_POST['desc']) ? sanitize_textarea_field(wp_unslash((string) $_POST['desc'])) : '';
$msg = isset($_POST['msg']) ? sanitize_textarea_field(wp_unslash((string) $_POST['msg'])) : '';
$type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : '';
$allowed_types = array_column((array) $s['types'], 'slug');
$type_settings = self::type_settings($type);
if ($this->text_length($name) < 4 || $this->text_length($name) > 16) {
wp_send_json_error(['message' => __('认证身份长度需在 4-16 个字符之间', 'aksrt')], 422);
}
if ($this->text_length($desc) > 300 || $this->text_length($msg) > 500) {
wp_send_json_error(['message' => __('认证说明内容过长', 'aksrt')], 422);
}
if ($type === '' || !in_array($type, $allowed_types, true)) {
if ($type_settings === []) {
wp_send_json_error(['message' => __('请选择有效的认证类型', 'aksrt')], 422);
}
if (!empty($type_settings['require_real_name']) && $real_name === '') {
wp_send_json_error(['message' => __('请填写真实姓名', 'aksrt')], 422);
}
if ($this->text_length($real_name) > 50) {
wp_send_json_error(['message' => __('真实姓名不能超过 50 个字符', 'aksrt')], 422);
}
$lock_key = 'aksrt_auth_apply_lock_' . $user_id;
$lock_token = wp_generate_uuid4();
@@ -472,26 +449,52 @@ class AuthApply {
}
$this->renew_submission_lock($lock_key, $lock_token);
$free_applications = max(0, (int) ($type_settings['free_applications'] ?? 0));
$application_count = self::application_count($user_id, $type);
$fee = $application_count < $free_applications ? 0.0 : max(0, (float) ($type_settings['fee'] ?? 0));
if ($fee > 0) {
$deducted = \AKSRT\Credits\Balance::deduct(
$user_id,
$fee,
'verify_application',
['type' => 'verify', 'id' => $user_id],
sprintf(__('提交%s申请', 'aksrt'), (string) $type_settings['label'])
);
if (is_wp_error($deducted)) {
$this->delete_attachments($uploaded);
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => $deducted->get_error_message()], 422);
}
}
$previous_payload = self::get_apply_payload($user_id);
$requested_at = current_time('Y-m-d H:i');
$payload = [
'name' => $name,
'real_name' => $real_name,
'desc' => $desc,
'msg' => $msg,
'type' => $type,
'evidence_attachment_ids' => $uploaded,
'time' => $requested_at,
'fee' => $fee,
];
$payload_saved = update_user_meta($user_id, self::APPLY_META, $payload);
if ($payload_saved === false && get_user_meta($user_id, self::APPLY_META, true) !== $payload) {
$this->delete_attachments($uploaded);
if ($fee > 0) {
\AKSRT\Credits\Balance::add($user_id, $fee, 'verify_application_refund', ['type' => 'verify', 'id' => $user_id], __('认证申请保存失败退款', 'aksrt'));
}
$this->release_submission_lock($lock_key, $lock_token);
wp_send_json_error(['message' => __('认证申请保存失败,请重试', 'aksrt')], 500);
}
$status_saved = update_user_meta($user_id, self::STATUS_META, 'pending');
if ($status_saved === false && self::get_status($user_id) !== 'pending') {
$this->delete_attachments($uploaded);
if ($fee > 0) {
\AKSRT\Credits\Balance::add($user_id, $fee, 'verify_application_refund', ['type' => 'verify', 'id' => $user_id], __('认证申请保存失败退款', 'aksrt'));
}
$previous_payload === []
? delete_user_meta($user_id, self::APPLY_META)
: update_user_meta($user_id, self::APPLY_META, $previous_payload);
@@ -502,6 +505,7 @@ class AuthApply {
delete_user_meta($user_id, self::REVIEWED_AT_META);
delete_user_meta($user_id, self::REVIEWER_META);
delete_user_meta($user_id, self::REASON_META);
self::increment_application_count($user_id, $type);
if (!$this->owns_submission_lock($lock_key, $lock_token)) {
$this->release_submission_lock($lock_key, $lock_token);
+14 -6
View File
@@ -139,12 +139,20 @@ class Home {
$signals = [];
if (!empty($settings['verify']['enabled']) && !empty($settings['verify']['public_display']) && $this->is_user_verified($user_id, $settings['verify'])) {
$signals[] = [
'type' => 'verify',
'label' => (string) $settings['verify']['badge_text'],
'icon' => 'check-circle',
'tone' => 'success',
];
$info = class_exists('AKSRT\\User\\AuthApply') ? AuthApply::get_auth_info($user_id) : [];
$type = sanitize_key((string) ($info['type'] ?? ''));
$type_settings = is_array($settings['verify']['types'][$type] ?? null) ? $settings['verify']['types'][$type] : [];
if ($type_settings !== []) {
$label = (string) $type_settings['badge_text'];
$signals[] = [
'type' => 'verify',
'label' => $label,
'title' => $label,
'icon' => 'check-circle',
'icon_url' => get_theme_file_uri('/assets/img/user/user-certification.svg'),
'tone' => 'success',
];
}
}
if (!empty($settings['level']['enabled']) && !empty($settings['level']['public_display'])) {