package com.yaoai.admin.page;

import cn.hutool.crypto.digest.BCrypt;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yaoai.admin.auth.StpAdminUtil;
import com.yaoai.common.exception.BizException;
import com.yaoai.common.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class AdminPageDataService {

    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    public Map<String, Object> getPageData(String pageKey) {
        if ("reports".equals(pageKey)) {
            return buildReportsPageData();
        }
        return switch (pageKey) {
            case "tenantAudits" -> buildTenantAuditsPageData();
            case "permissionConfig" -> buildPermissionConfigPageData();
            case "modelConfig" -> buildModelConfigPageData();
            case "ordersInvoices" -> buildOrdersInvoicesPageData();
            case "orderOperationLogs" -> buildOrderOperationLogsPageData();
            case "warnings" -> buildWarningsPageData();
            case "tickets" -> buildTicketsPageData();
            case "scriptLibrary" -> buildScriptLibraryPageData();
            case "supportPerformance" -> buildSupportPerformancePageData();
            case "manuals" -> buildManualsPageData();
            case "announcements" -> buildAnnouncementsPageData();
            case "globalSettings" -> buildGlobalSettingsPageData();
            default -> readPage(pageKey);
        };
    }

    public Map<String, Object> getPageSettings(String pageKey) {
        String key = settingsKey(pageKey);
        String payload = jdbcTemplate.query(
                "select setting_value from system_settings where setting_key = ?",
                rs -> rs.next() ? rs.getString(1) : null,
                key);
        if (!StringUtils.hasText(payload)) {
            return defaultSettings(pageKey);
        }
        try {
            return objectMapper.readValue(payload, new TypeReference<>() {
            });
        } catch (Exception e) {
            throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "页面参数格式错误");
        }
    }

    public Map<String, Object> savePageSettings(String pageKey, Map<String, Object> settings) {
        String key = settingsKey(pageKey);
        Map<String, Object> payloadMap = new LinkedHashMap<>(defaultSettings(pageKey));
        payloadMap.putAll(settings);
        try {
            String payload = objectMapper.writeValueAsString(payloadMap);
            long adminId = StpAdminUtil.getLoginIdAsLong();
            int updated = jdbcTemplate.update(
                    "update system_settings set setting_value = ?, updated_at = CURRENT_TIMESTAMP, updated_by = ? where setting_key = ?",
                    payload,
                    adminId,
                    key);
            if (updated == 0) {
                jdbcTemplate.update(
                        "insert into system_settings (id, setting_key, setting_value, updated_at, updated_by) values (?, ?, ?, ?, ?)",
                        System.currentTimeMillis(),
                        key,
                        payload,
                        LocalDateTime.now(),
                        adminId);
            }
            return getPageSettings(pageKey);
        } catch (Exception e) {
            throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "页面参数保存失败");
        }
    }

    public List<Map<String, Object>> getReportRules() {
        List<Map<String, Object>> rules = jdbcTemplate.queryForList("""
                select code, name, scope, formula, owner, editable, notes, status, sort_order, updated_at
                  from admin_report_rules
                 order by sort_order asc, code asc
                """);
        return rules.stream().map(row -> {
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("code", value(row, "code"));
            next.put("name", businessText(value(row, "name")));
            next.put("scope", businessText(value(row, "scope")));
            next.put("formula", businessText(value(row, "formula")));
            next.put("owner", businessText(value(row, "owner")));
            next.put("editable", businessText(value(row, "editable")));
            next.put("notes", businessText(value(row, "notes")));
            next.put("status", number(row, "status").intValue());
            next.put("sortOrder", number(row, "sort_order").intValue());
            next.put("updatedAt", formatDateTime(time(row, "updated_at")));
            return next;
        }).toList();
    }

    public List<Map<String, Object>> saveReportRules(List<Map<String, Object>> rules) {
        if (rules == null || rules.isEmpty()) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "报表规则不能为空");
        }
        Set<String> submittedCodes = new HashSet<>();
        int fallbackSort = 10;
        for (Map<String, Object> rule : rules) {
            String code = requestText(rule, "code");
            if (!StringUtils.hasText(code)) {
                code = "rule_" + System.currentTimeMillis() + "_" + fallbackSort;
            }
            String name = requestText(rule, "name");
            String scope = businessText(requestText(rule, "scope"));
            String formula = businessText(requestText(rule, "formula"));
            if (!StringUtils.hasText(name) || !StringUtils.hasText(scope) || !StringUtils.hasText(formula)) {
                throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "规则名称、统计口径和计算公式不能为空");
            }
            String owner = requestText(rule, "owner");
            String editable = requestText(rule, "editable");
            String notes = businessText(requestText(rule, "notes"));
            int status = requestInt(rule, "status", 1);
            int sortOrder = requestInt(rule, "sortOrder", fallbackSort);
            submittedCodes.add(code);

            int updated = jdbcTemplate.update("""
                    update admin_report_rules
                       set name = ?, scope = ?, formula = ?, owner = ?, editable = ?, notes = ?,
                           status = ?, sort_order = ?, updated_at = CURRENT_TIMESTAMP
                     where code = ?
                    """,
                    name,
                    scope,
                    formula,
                    StringUtils.hasText(owner) ? owner : "系统",
                    StringUtils.hasText(editable) ? editable : "需管理员确认",
                    notes,
                    status,
                    sortOrder,
                    code);
            if (updated == 0) {
                jdbcTemplate.update("""
                        insert into admin_report_rules (code, name, scope, formula, owner, editable, notes, status, sort_order, updated_at)
                        values (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
                        """,
                        code,
                        name,
                        scope,
                        formula,
                        StringUtils.hasText(owner) ? owner : "系统",
                        StringUtils.hasText(editable) ? editable : "需管理员确认",
                        notes,
                        status,
                        sortOrder);
            }
            fallbackSort += 10;
        }

        List<String> existingCodes = jdbcTemplate.queryForList("select code from admin_report_rules", String.class);
        for (String existingCode : existingCodes) {
            if (!submittedCodes.contains(existingCode)) {
                jdbcTemplate.update(
                        "update admin_report_rules set status = 0, updated_at = CURRENT_TIMESTAMP where code = ?",
                        existingCode);
            }
        }
        return getReportRules();
    }

    public Map<String, Object> addRow(String pageKey, Map<String, Object> row) {
        if ("tenantAudits".equals(pageKey)) {
            addTenantAuditApplication(row);
            return buildTenantAuditsPageData();
        }
        if ("permissionConfig".equals(pageKey)) {
            addPermissionConfiguration(row);
            return buildPermissionConfigPageData();
        }
        if ("modelConfig".equals(pageKey)) {
            addModelConfiguration(row);
            return buildModelConfigPageData();
        }
        if ("scriptLibrary".equals(pageKey)) {
            addScript(row);
            return buildScriptLibraryPageData();
        }
        if ("manuals".equals(pageKey) || "announcements".equals(pageKey)) {
            addContentArticle(pageKey, row);
            return getPageData(pageKey);
        }
        if ("globalSettings".equals(pageKey)) {
            addGlobalSetting(row);
            return buildGlobalSettingsPageData();
        }
        if ("tickets".equals(pageKey)) {
            addTicket(row);
            return buildTicketsPageData();
        }
        if (List.of("ordersInvoices", "orderOperationLogs", "warnings", "reports", "supportPerformance").contains(pageKey)) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "该模块由真实业务数据自动生成，不能手工新增");
        }
        Map<String, Object> page = readPage(pageKey);
        List<Map<String, Object>> rows = rowsOf(page);
        Map<String, Object> next = new LinkedHashMap<>(row);
        next.putIfAbsent("id", String.valueOf(System.currentTimeMillis()));
        next.putIfAbsent("status", "待处理");
        next.put("createdAt", LocalDateTime.now().toString());
        rows.add(0, next);
        page.put("rows", rows);
        writePage(pageKey, page);
        return page;
    }

    public Map<String, Object> updateRow(String pageKey, int rowIndex, Map<String, Object> row) {
        if ("tenantAudits".equals(pageKey)) {
            updateTenantAuditApplication(rowIndex, row);
            return buildTenantAuditsPageData();
        }
        if ("permissionConfig".equals(pageKey)) {
            Long id = rowId(permissionConfigRows(), rowIndex);
            updatePermissionConfiguration(id, row);
            return buildPermissionConfigPageData();
        }
        if ("modelConfig".equals(pageKey)) {
            Long id = rowId(modelConfigRows(), rowIndex);
            updateModelConfiguration(id, row);
            return buildModelConfigPageData();
        }
        if ("scriptLibrary".equals(pageKey)) {
            Long id = rowId(scriptRows(), rowIndex);
            updateScript(id, row);
            return buildScriptLibraryPageData();
        }
        if ("manuals".equals(pageKey) || "announcements".equals(pageKey)) {
            Long id = rowId(contentArticleRows(pageKey), rowIndex);
            updateContentArticle(id, pageKey, row);
            return getPageData(pageKey);
        }
        if ("globalSettings".equals(pageKey)) {
            Long id = rowId(globalSettingRows(), rowIndex);
            updateGlobalSetting(id, row);
            return buildGlobalSettingsPageData();
        }
        if ("tickets".equals(pageKey)) {
            Long id = rowId(ticketRows(), rowIndex);
            updateTicket(id, row);
            return buildTicketsPageData();
        }
        if (List.of("ordersInvoices", "orderOperationLogs", "warnings", "reports", "supportPerformance").contains(pageKey)) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "该模块由真实业务数据自动生成，请到对应业务入口处理");
        }
        Map<String, Object> page = readPage(pageKey);
        List<Map<String, Object>> rows = rowsOf(page);
        if (rowIndex < 0 || rowIndex >= rows.size()) {
            throw new BizException(ErrorCode.NOT_FOUND.getCode(), "记录不存在");
        }
        Map<String, Object> existing = new LinkedHashMap<>(rows.get(rowIndex));
        existing.putAll(row);
        existing.putIfAbsent("id", rows.get(rowIndex).get("id"));
        existing.put("updatedAt", LocalDateTime.now().toString());
        rows.set(rowIndex, existing);
        page.put("rows", rows);
        writePage(pageKey, page);
        return page;
    }

    public Map<String, Object> deleteRow(String pageKey, int rowIndex) {
        if ("tenantAudits".equals(pageKey)) {
            rejectTenantAuditApplication(rowIndex);
            return buildTenantAuditsPageData();
        }
        if ("permissionConfig".equals(pageKey)) {
            Long id = rowId(permissionConfigRows(), rowIndex);
            jdbcTemplate.update("update tenant_member_permissions set status = 0, revoked_at = CURRENT_TIMESTAMP, remark = '运营后台停用权限' where id = ?", id);
            return buildPermissionConfigPageData();
        }
        if ("modelConfig".equals(pageKey)) {
            Long id = rowId(modelConfigRows(), rowIndex);
            jdbcTemplate.update("delete from tenant_model_defaults where provider_id = ?", id);
            jdbcTemplate.update("delete from tenant_model_providers where id = ?", id);
            return buildModelConfigPageData();
        }
        if ("scriptLibrary".equals(pageKey)) {
            Long id = rowId(scriptRows(), rowIndex);
            jdbcTemplate.update("update customer_scripts set status = '已下线', deleted_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP where id = ?", id);
            return buildScriptLibraryPageData();
        }
        if ("manuals".equals(pageKey) || "announcements".equals(pageKey)) {
            Long id = rowId(contentArticleRows(pageKey), rowIndex);
            jdbcTemplate.update("update content_articles set status = '已下线', deleted_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP where id = ?", id);
            return getPageData(pageKey);
        }
        if ("globalSettings".equals(pageKey)) {
            Long id = rowId(globalSettingRows(), rowIndex);
            jdbcTemplate.update("update system_settings set status = '已停用', updated_at = CURRENT_TIMESTAMP, updated_by = ? where id = ?", currentAdminId(), id);
            return buildGlobalSettingsPageData();
        }
        if ("tickets".equals(pageKey)) {
            Long id = rowId(ticketRows(), rowIndex);
            jdbcTemplate.update("update customer_tickets set status = '已关闭', updated_at = CURRENT_TIMESTAMP where id = ?", id);
            return buildTicketsPageData();
        }
        if (List.of("ordersInvoices", "warnings", "reports", "supportPerformance").contains(pageKey)) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "该模块不支持删除事实数据，请到源业务表做状态处理");
        }
        Map<String, Object> page = readPage(pageKey);
        List<Map<String, Object>> rows = rowsOf(page);
        if (rowIndex < 0 || rowIndex >= rows.size()) {
            throw new BizException(ErrorCode.NOT_FOUND.getCode(), "记录不存在");
        }
        rows.remove(rowIndex);
        page.put("rows", rows);
        writePage(pageKey, page);
        return page;
    }

    public Map<String, Object> processRow(String pageKey, int rowIndex) {
        if ("scriptLibrary".equals(pageKey)) {
            Long id = rowId(scriptRows(), rowIndex);
            jdbcTemplate.update("update customer_scripts set status = '已发布', updated_at = CURRENT_TIMESTAMP where id = ?", id);
            return buildScriptLibraryPageData();
        }
        if ("manuals".equals(pageKey) || "announcements".equals(pageKey)) {
            Long id = rowId(contentArticleRows(pageKey), rowIndex);
            jdbcTemplate.update("update content_articles set status = '已发布', publish_at = coalesce(publish_at, CURRENT_TIMESTAMP), updated_at = CURRENT_TIMESTAMP where id = ?", id);
            return getPageData(pageKey);
        }
        if ("globalSettings".equals(pageKey)) {
            Long id = rowId(globalSettingRows(), rowIndex);
            jdbcTemplate.update("update system_settings set status = '启用', effective_at = coalesce(effective_at, CURRENT_TIMESTAMP), updated_at = CURRENT_TIMESTAMP, updated_by = ? where id = ?", currentAdminId(), id);
            return buildGlobalSettingsPageData();
        }
        if ("permissionConfig".equals(pageKey)) {
            Long id = rowId(permissionConfigRows(), rowIndex);
            jdbcTemplate.update("update tenant_member_permissions set status = 1, revoked_at = null, granted_at = CURRENT_TIMESTAMP, granted_by = ? where id = ?", currentAdminId(), id);
            return buildPermissionConfigPageData();
        }
        if ("tickets".equals(pageKey)) {
            Long id = rowId(ticketRows(), rowIndex);
            jdbcTemplate.update("""
                    update customer_tickets
                       set status = '已解决',
                           resolved_at = coalesce(resolved_at, CURRENT_TIMESTAMP),
                           updated_at = CURRENT_TIMESTAMP
                     where id = ?
                    """, id);
            return buildTicketsPageData();
        }
        if ("tenantAudits".equals(pageKey)) {
            processTenantAuditApplication(rowIndex);
            return buildTenantAuditsPageData();
        }
        if (List.of("ordersInvoices", "orderOperationLogs", "warnings", "reports", "supportPerformance").contains(pageKey)) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "该模块由业务规则自动计算，没有可手工处理的行状态");
        }
        Map<String, Object> page = readPage(pageKey);
        List<Map<String, Object>> rows = rowsOf(page);
        if (rowIndex < 0 || rowIndex >= rows.size()) {
            throw new BizException(ErrorCode.NOT_FOUND.getCode(), "记录不存在");
        }
        Map<String, Object> row = new LinkedHashMap<>(rows.get(rowIndex));
        row.put("status", processedStatus(pageKey));
        row.put("handledAt", LocalDateTime.now().toString());
        rows.set(rowIndex, row);
        page.put("rows", rows);
        writePage(pageKey, page);
        return page;
    }

    public Map<String, Object> saveFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "上传文件不能为空");
        }
        try {
            String originalName = StringUtils.cleanPath(file.getOriginalFilename() == null ? "upload" : file.getOriginalFilename());
            String ext = "";
            int dotIndex = originalName.lastIndexOf('.');
            if (dotIndex >= 0) {
                ext = originalName.substring(dotIndex);
            }
            String storedName = UUID.randomUUID() + ext;
            Path uploadDir = Path.of(".local-data", "admin-uploads").toAbsolutePath().normalize();
            Files.createDirectories(uploadDir);
            Path target = uploadDir.resolve(storedName);
            Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);

            Map<String, Object> result = new HashMap<>();
            result.put("name", originalName);
            result.put("storedName", storedName);
            result.put("size", file.getSize());
            result.put("contentType", file.getContentType());
            result.put("path", target.toString());
            return result;
        } catch (Exception e) {
            throw new BizException(ErrorCode.UPLOAD_FAILED.getCode(), "文件上传失败");
        }
    }

    private String processedStatus(String pageKey) {
        return switch (pageKey) {
            case "tenantAudits" -> "已通过";
            case "ordersInvoices" -> "已归档";
            case "warnings" -> "已关闭";
            case "reports" -> "已生成";
            case "tickets" -> "已解决";
            case "scriptLibrary", "manuals", "announcements" -> "已发布";
            case "supportPerformance" -> "已复盘";
            case "globalSettings" -> "已生效";
            case "permissionConfig", "modelConfig" -> "启用";
            default -> "已处理";
        };
    }

    private String settingsKey(String pageKey) {
        if ("warnings".equals(pageKey)) {
            return "warnings.rules";
        }
        return pageKey + ".settings";
    }

    private Map<String, Object> defaultSettings(String pageKey) {
        if (!"warnings".equals(pageKey)) {
            return new LinkedHashMap<>();
        }
        Map<String, Object> settings = new LinkedHashMap<>();
        settings.put("creditLowPercent", 20);
        settings.put("creditCriticalPercent", 10);
        settings.put("dormantDays", 30);
        settings.put("expireSoonDays", 7);
        settings.put("taskFailRatePercent", 15);
        settings.put("ticketSlaHours", 4);
        settings.put("invoicePendingDays", 3);
        settings.put("auditPendingHours", 2);
        settings.put("contentBlockTimes", 3);
        settings.put("channels", List.of("站内信", "短信", "邮件", "企业微信"));
        settings.put("remindTimes", List.of("09:30", "16:30"));
        settings.put("escalationOwners", List.of("运营一组", "财务值班", "技术值班", "客服主管", "内容运营"));
        return settings;
    }

    private Map<String, Object> readPage(String pageKey) {
        String payload = jdbcTemplate.query(
                "select payload_json from admin_page_data where page_key = ? and status = 1",
                rs -> rs.next() ? rs.getString(1) : null,
                pageKey);
        if (payload == null) {
            throw new BizException(ErrorCode.NOT_FOUND.getCode(), "页面数据未配置");
        }
        try {
            return objectMapper.readValue(payload, new TypeReference<>() {
            });
        } catch (Exception e) {
            throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "页面数据格式错误");
        }
    }

    private Map<String, Object> buildReportsPageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        page.put("title", "数据报表中心");
        page.put("desc", "所有指标从业务数据库实时聚合，算法口径从报表规则表读取");
        page.put("primary", "生成月报");
        page.put("allowCreate", false);
        page.put("allowEdit", false);
        page.put("allowDelete", false);
        page.put("allowProcess", false);
        page.put("workflow", List.of("读取业务表", "指标聚合", "规则校验", "趋势分析", "输出报表"));
        page.put("metrics", reportMetrics());
        page.put("filters", List.of("报表状态", "数据来源", "负责人", "统计口径"));
        page.put("columns", List.of(
                column("name", "报表名称", 180, false),
                column("scope", "统计口径", 220, false),
                column("dataSource", "数据来源", 220, true),
                column("updatedAt", "更新时间", 150, false),
                column("owner", "负责人", 110, true),
                column("status", "状态", 110, true)
        ));
        page.put("rows", reportRows());
        page.put("logicRules", reportRules());
        page.put("cards", reportCards());
        return page;
    }

    private Map<String, Object> buildTenantAuditsPageData() {
        ensureTenantOnboardingTable();
        Map<String, Object> page = new LinkedHashMap<>();
        long pending = count("""
                select count(*)
                  from tenant_onboarding_applications
                 where status in ('资料待审', '人工复核', '待开通')
                """);
        long needSupplement = count("""
                select count(*)
                  from tenant_onboarding_applications
                 where status = '待补件'
                    or license_status = '待补件'
                    or identity_status = '待补件'
                    or authorization_status = '待补件'
                """);
        long approvedToday = count("""
                select count(*)
                  from tenant_onboarding_applications
                 where status = '已通过'
                   and reviewed_at >= ?
                """, LocalDateTime.now().toLocalDate().atStartOfDay());
        long activated = count("""
                select count(*)
                  from tenant_onboarding_applications a
                  join tenants t on t.id = a.tenant_id
                 where a.status = '已通过'
                   and t.status = 1
                   and t.deleted_at is null
                """);
        page.put("title", "入驻审核中心");
        page.put("desc", "按企业入驻平台标准核验主体资质、证照材料、法人/经办人身份、Owner 账号和试用资源开通");
        page.put("primary", "新建入驻申请");
        page.put("allowCreate", true);
        page.put("allowEdit", true);
        page.put("allowDelete", true);
        page.put("allowProcess", true);
        page.put("workflow", List.of("资料提交", "证照核验", "法人核身", "试用开通", "审计归档"));
        page.put("metrics", List.of(
                metric("待审核", String.valueOf(pending), "资料、证照与法人核验"),
                metric("需补件", String.valueOf(needSupplement), "执照、身份或授权书缺失"),
                metric("今日通过", String.valueOf(approvedToday), "已激活企业与 Owner"),
                metric("已开通", String.valueOf(activated), "可登录客户端使用")
        ));
        page.put("filters", List.of("审核状态", "证照状态", "法人核身", "试用套餐"));
        page.put("columns", List.of(
                column("company", "企业名称", 190, false),
                column("creditCode", "统一社会信用代码", 220, false),
                column("legalPerson", "法人/Owner", 150, false),
                column("licenseStatus", "营业执照", 120, true),
                column("identityStatus", "身份核验", 140, true),
                column("contact", "联系人", 170, false),
                column("trialPlan", "拟开通套餐", 130, true),
                column("risk", "风险结论", 130, true),
                column("status", "审核状态", 120, true)
        ));
        page.put("rows", tenantAuditRows());
        page.put("cards", List.of(
                card("企业入驻标准", "主体 + 证照 + 法人 + 授权",
                        List.of("统一社会信用代码需要唯一且格式有效",
                                "营业执照、法人身份材料为必传",
                                "经办人与法人不一致时必须提交授权书")),
                card("审核通过动作", "激活租户 + Owner + 试用资源",
                        List.of("前端提交时创建待审核租户与禁用 Owner",
                                "审核通过后激活客户端登录权限",
                                "按套餐写入配额、余额与入驻试用入账流水")),
                card("审计归档", "状态、意见和处理人留痕",
                        List.of("补件、驳回、通过都回写入驻申请表",
                                "通过动作写入订单/资源流水和运营审计",
                                "历史已开通租户保留为只读归档"))
        ));
        return page;
    }

    private List<Map<String, Object>> tenantAuditRows() {
        ensureTenantOnboardingTable();
        List<Map<String, Object>> applicationRows = jdbcTemplate.queryForList("""
                select a.*, t.status as tenant_status, t.slug, u.status as owner_status
                  from tenant_onboarding_applications a
                  left join tenants t on t.id = a.tenant_id
                  left join users u on u.id = a.owner_user_id
                 order by
                       case a.status
                         when '资料待审' then 1
                         when '人工复核' then 2
                         when '待补件' then 3
                         when '待开通' then 4
                         when '已通过' then 5
                         else 6
                       end,
                       a.updated_at desc,
                       a.created_at desc
                """);
        List<Map<String, Object>> rows = new ArrayList<>(applicationRows.stream().map(row -> {
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("id", "APP-" + text(row, "id"));
            next.put("company", text(row, "company_name"));
            next.put("creditCode", text(row, "credit_code"));
            next.put("legalPerson", text(row, "legal_person_name") + " / " + text(row, "owner_username"));
            next.put("ownerUsername", text(row, "owner_username"));
            next.put("licenseStatus", text(row, "license_status"));
            next.put("identityStatus", text(row, "identity_status"));
            next.put("authorizationStatus", text(row, "authorization_status"));
            next.put("contact", text(row, "contact_name") + " " + text(row, "contact_phone"));
            next.put("phone", text(row, "contact_phone"));
            next.put("contactEmail", text(row, "contact_email"));
            next.put("ownerEmail", text(row, "owner_email"));
            next.put("industry", text(row, "industry"));
            next.put("companyScale", text(row, "company_scale"));
            next.put("businessScope", text(row, "business_scope"));
            next.put("licenseFile", text(row, "license_file"));
            next.put("identityFile", text(row, "identity_file"));
            next.put("authorizationFile", text(row, "authorization_file"));
            next.put("submittedAt", formatDateTime(time(row, "created_at")));
            next.put("reviewedAt", formatDateTime(time(row, "reviewed_at")));
            next.put("trialPlan", text(row, "trial_plan"));
            next.put("risk", text(row, "risk_level"));
            next.put("riskReason", text(row, "risk_reason"));
            next.put("reviewNote", text(row, "review_note"));
            next.put("status", text(row, "status"));
            next.put("source", text(row, "source"));
            next.put("tenantId", text(row, "tenant_id"));
            return next;
        }).toList());

        List<Map<String, Object>> tenantRows = jdbcTemplate.queryForList("""
                select t.id, t.name, t.slug, t.plan, t.status, t.notes, t.created_at,
                       u.username, u.email, u.phone
                  from tenants t
                  left join tenant_members tm on tm.tenant_id = t.id and tm.role = 'owner'
                  left join users u on u.id = tm.user_id and u.deleted_at is null
                 where t.deleted_at is null
                   and not exists (
                       select 1 from tenant_onboarding_applications a where a.tenant_id = t.id
                   )
                 order by t.updated_at desc, t.created_at desc
                 limit 20
                """);
        tenantRows.forEach(row -> {
            boolean active = number(row, "status").intValue() == 1;
            boolean hasOwner = StringUtils.hasText(text(row, "username"));
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("id", "TENANT-" + text(row, "id"));
            next.put("company", text(row, "name"));
            next.put("creditCode", "历史开通 / " + text(row, "slug"));
            next.put("legalPerson", StringUtils.hasText(text(row, "username")) ? text(row, "username") : "未绑定");
            next.put("licenseStatus", active ? "历史已验" : "待复核");
            next.put("identityStatus", hasOwner ? "Owner 已绑定" : "缺少 Owner");
            next.put("authorizationStatus", "历史归档");
            next.put("contact", StringUtils.hasText(text(row, "email")) ? text(row, "email") : text(row, "phone"));
            next.put("submittedAt", formatDateTime(time(row, "created_at")));
            next.put("trialPlan", planLabel(text(row, "plan")));
            next.put("risk", active && hasOwner ? "低风险" : "需人工确认");
            next.put("status", active && hasOwner ? "已通过" : "人工复核");
            next.put("reviewNote", text(row, "notes"));
            next.put("source", "历史租户");
            next.put("tenantId", text(row, "id"));
            rows.add(next);
        });
        return rows;
    }

    private void ensureTenantOnboardingTable() {
        jdbcTemplate.execute("""
                create table if not exists tenant_onboarding_applications
                (
                    id bigint not null primary key,
                    tenant_id bigint,
                    owner_user_id bigint,
                    company_name varchar(160) not null,
                    credit_code varchar(32) not null,
                    legal_person_name varchar(80) not null,
                    agent_name varchar(80),
                    contact_name varchar(80) not null,
                    contact_phone varchar(40),
                    contact_email varchar(160),
                    owner_username varchar(64) not null,
                    owner_email varchar(160) not null,
                    industry varchar(80),
                    company_scale varchar(40),
                    business_scope text,
                    trial_plan varchar(40) not null default 'PRO_TRIAL',
                    license_file varchar(500),
                    identity_file varchar(500),
                    authorization_file varchar(500),
                    license_status varchar(40) not null default '待验真',
                    identity_status varchar(40) not null default '待核身',
                    authorization_status varchar(40) not null default '待确认',
                    risk_level varchar(40) not null default '需人工确认',
                    risk_reason varchar(255),
                    status varchar(40) not null default '资料待审',
                    review_note text,
                    reviewed_by bigint,
                    reviewed_at datetime(3),
                    source varchar(40) not null default '运营录入',
                    created_at datetime(3) not null default current_timestamp(3),
                    updated_at datetime(3) not null default current_timestamp(3)
                )
                """);
    }

    private void addTenantAuditApplication(Map<String, Object> row) {
        ensureTenantOnboardingTable();
        String company = requiredText(row, "company", "企业名称不能为空");
        String creditCode = cleanCreditCode(requiredText(row, "creditCode", "统一社会信用代码不能为空"));
        String legalPerson = requiredText(row, "legalPerson", "法人姓名不能为空");
        String ownerUsername = requiredText(row, "ownerUsername", "Owner 账号不能为空");
        String ownerEmail = requiredText(row, "ownerEmail", "Owner 登录邮箱不能为空").toLowerCase();
        String contactName = requiredText(row, "contact", "联系人不能为空");
        String contactPhone = requestText(row, "phone");
        String ownerPassword = requestText(row, "ownerPassword");
        if (!StringUtils.hasText(ownerPassword)) {
            ownerPassword = "Dramix@123";
        }
        if (count("select count(*) from tenant_onboarding_applications where credit_code = ? and status <> '已驳回'", creditCode) > 0) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "该统一社会信用代码已有入驻申请");
        }
        if (count("select count(*) from users where email = ? and deleted_at is null", ownerEmail) > 0
                || count("select count(*) from tenant_onboarding_applications where owner_email = ? and status <> '已驳回'", ownerEmail) > 0) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "Owner 邮箱已存在或已有待审核申请");
        }

        String planCode = planCodeFromTrial(requestText(row, "trialPlan"));
        Map<String, Object> plan = planRow(planCode);
        long tenantId = newBusinessId();
        long ownerUserId = newBusinessId();
        long memberId = newBusinessId();
        long applicationId = newBusinessId();
        LocalDateTime now = LocalDateTime.now();
        String licenseFile = requestText(row, "licenseFile");
        String identityFile = requestText(row, "identityFile");
        String authorizationFile = requestText(row, "authorizationFile");
        boolean needsAuthorization = StringUtils.hasText(requestText(row, "agentName")) && !requestText(row, "agentName").equals(legalPerson);
        String status = hasRequiredMaterials(licenseFile, identityFile, authorizationFile, needsAuthorization) ? "资料待审" : "待补件";
        String riskLevel = "资料待审".equals(status) ? "低风险" : "需补件";
        String riskReason = materialRiskReason(licenseFile, identityFile, authorizationFile, needsAuthorization);

        jdbcTemplate.update("""
                insert into tenants (id, name, slug, plan, status, credits, frozen_credits, notes, created_at, updated_at)
                values (?, ?, ?, ?, 0, 0, 0, ?, ?, ?)
                """,
                tenantId,
                company,
                uniqueTenantSlug(creditCode),
                planCode.toLowerCase(),
                "企业入驻待审核：" + creditCode,
                now,
                now);
        jdbcTemplate.update("""
                insert into users (id, tenant_id, username, email, phone, password_hash, role, status, created_at, updated_at)
                values (?, ?, ?, ?, ?, ?, 'owner', 0, ?, ?)
                """,
                ownerUserId,
                tenantId,
                ownerUsername,
                ownerEmail,
                contactPhone,
                BCrypt.hashpw(ownerPassword, BCrypt.gensalt()),
                now,
                now);
        jdbcTemplate.update("""
                insert into tenant_members (id, tenant_id, user_id, role, position, joined_at)
                values (?, ?, ?, 'owner', '企业管理员', ?)
                """,
                memberId,
                tenantId,
                ownerUserId,
                now);
        jdbcTemplate.update("""
                insert into tenant_balances (id, tenant_id, balance, total_recharged, total_spent, version, updated_at)
                values (?, ?, 0, 0, 0, 0, ?)
                """,
                newBusinessId(),
                tenantId,
                now);
        jdbcTemplate.update("""
                insert into tenant_quotas
                (tenant_id, max_members, max_projects, max_monthly_credits, max_concurrent_tasks, notes, updated_at, updated_by)
                values (?, ?, ?, ?, ?, ?, ?, ?)
                """,
                tenantId,
                number(plan, "max_members").intValue(),
                number(plan, "max_projects").intValue(),
                number(plan, "monthly_credits").intValue(),
                number(plan, "max_concurrent_tasks").intValue(),
                "入驻审核通过后按 " + text(plan, "name") + " 开通试用资源",
                now,
                currentAdminId());
        jdbcTemplate.update("""
                insert into tenant_onboarding_applications
                (id, tenant_id, owner_user_id, company_name, credit_code, legal_person_name, agent_name,
                 contact_name, contact_phone, contact_email, owner_username, owner_email, industry, company_scale,
                 business_scope, trial_plan, license_file, identity_file, authorization_file, license_status,
                 identity_status, authorization_status, risk_level, risk_reason, status, review_note, source, created_at, updated_at)
                values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '运营录入', ?, ?)
                """,
                applicationId,
                tenantId,
                ownerUserId,
                company,
                creditCode,
                legalPerson,
                requestText(row, "agentName"),
                contactName,
                contactPhone,
                requestText(row, "contactEmail"),
                ownerUsername,
                ownerEmail,
                requestText(row, "industry"),
                requestText(row, "companyScale"),
                requestText(row, "businessScope"),
                trialPlanLabel(planCode),
                licenseFile,
                identityFile,
                authorizationFile,
                StringUtils.hasText(licenseFile) ? "待验真" : "待补件",
                StringUtils.hasText(identityFile) ? "待核身" : "待补件",
                needsAuthorization ? (StringUtils.hasText(authorizationFile) ? "待确认" : "待补件") : "无需授权",
                riskLevel,
                riskReason,
                status,
                requestText(row, "reviewNote"),
                now,
                now);
    }

    private void updateTenantAuditApplication(int rowIndex, Map<String, Object> row) {
        Map<String, Object> current = tenantAuditRows().get(rowIndex);
        String id = text(current, "id");
        if (id.startsWith("TENANT-")) {
            Long tenantId = tenantIdFromAuditRow(rowIndex);
            jdbcTemplate.update("""
                    update tenants
                       set name = ?,
                           notes = ?,
                           updated_at = CURRENT_TIMESTAMP
                     where id = ?
                    """,
                    StringUtils.hasText(requestText(row, "company")) ? requestText(row, "company") : text(current, "company"),
                    requestText(row, "reviewNote"),
                    tenantId);
            return;
        }

        Long applicationId = applicationIdFromAuditRow(rowIndex);
        String reviewConclusion = requestText(row, "reviewConclusion");
        String status = StringUtils.hasText(requestText(row, "status")) ? requestText(row, "status") : statusFromReviewConclusion(reviewConclusion, text(current, "status"));
        String licenseStatus = StringUtils.hasText(requestText(row, "licenseStatus")) ? requestText(row, "licenseStatus") : text(current, "licenseStatus");
        String identityStatus = StringUtils.hasText(requestText(row, "identityStatus")) ? requestText(row, "identityStatus") : text(current, "identityStatus");
        String authorizationStatus = StringUtils.hasText(requestText(row, "authorizationStatus")) ? requestText(row, "authorizationStatus") : text(current, "authorizationStatus");
        String risk = StringUtils.hasText(requestText(row, "risk")) ? requestText(row, "risk") : riskFromStatus(status);
        jdbcTemplate.update("""
                update tenant_onboarding_applications
                   set company_name = ?,
                       credit_code = ?,
                       legal_person_name = ?,
                       agent_name = ?,
                       contact_name = ?,
                       contact_phone = ?,
                       contact_email = ?,
                       owner_username = ?,
                       owner_email = ?,
                       industry = ?,
                       company_scale = ?,
                       business_scope = ?,
                       trial_plan = ?,
                       license_file = ?,
                       identity_file = ?,
                       authorization_file = ?,
                       license_status = ?,
                       identity_status = ?,
                       authorization_status = ?,
                       risk_level = ?,
                       risk_reason = ?,
                       status = ?,
                       review_note = ?,
                       reviewed_by = ?,
                       reviewed_at = case when ? in ('待补件', '已驳回') then CURRENT_TIMESTAMP else reviewed_at end,
                       updated_at = CURRENT_TIMESTAMP
                 where id = ?
                """,
                valueOrCurrent(row, current, "company"),
                cleanCreditCode(valueOrCurrent(row, current, "creditCode")),
                legalPersonName(valueOrCurrent(row, current, "legalPerson")),
                requestText(row, "agentName"),
                valueOrCurrent(row, current, "contact"),
                valueOrCurrent(row, current, "phone"),
                valueOrCurrent(row, current, "contactEmail"),
                valueOrCurrent(row, current, "ownerUsername"),
                valueOrCurrent(row, current, "ownerEmail").toLowerCase(),
                valueOrCurrent(row, current, "industry"),
                valueOrCurrent(row, current, "companyScale"),
                valueOrCurrent(row, current, "businessScope"),
                valueOrCurrent(row, current, "trialPlan"),
                valueOrCurrent(row, current, "licenseFile"),
                valueOrCurrent(row, current, "identityFile"),
                valueOrCurrent(row, current, "authorizationFile"),
                licenseStatus,
                identityStatus,
                authorizationStatus,
                risk,
                StringUtils.hasText(requestText(row, "riskReason")) ? requestText(row, "riskReason") : text(current, "riskReason"),
                status,
                StringUtils.hasText(requestText(row, "reviewNote")) ? requestText(row, "reviewNote") : requestText(row, "reviewConclusion"),
                currentAdminId(),
                status,
                applicationId);
    }

    private void rejectTenantAuditApplication(int rowIndex) {
        Map<String, Object> row = tenantAuditRows().get(rowIndex);
        String id = text(row, "id");
        if (id.startsWith("TENANT-")) {
            Long tenantId = tenantIdFromAuditRow(rowIndex);
            jdbcTemplate.update("""
                    update tenants
                       set status = 0,
                           suspended_at = CURRENT_TIMESTAMP,
                           suspended_by = ?,
                           suspended_reason = '入驻资料复核未通过',
                           notes = '历史租户在入驻审核中心被标记为未通过',
                           updated_at = CURRENT_TIMESTAMP
                     where id = ?
                    """, currentAdminId(), tenantId);
            return;
        }
        Long applicationId = applicationIdFromAuditRow(rowIndex);
        Map<String, Object> app = applicationRow(applicationId);
        jdbcTemplate.update("""
                update tenant_onboarding_applications
                   set status = '已驳回',
                       risk_level = '高风险',
                       review_note = coalesce(review_note, '入驻审核未通过'),
                       reviewed_by = ?,
                       reviewed_at = CURRENT_TIMESTAMP,
                       updated_at = CURRENT_TIMESTAMP
                 where id = ?
                """, currentAdminId(), applicationId);
        jdbcTemplate.update("update tenants set status = 0, suspended_at = CURRENT_TIMESTAMP, suspended_by = ?, suspended_reason = '入驻审核未通过', updated_at = CURRENT_TIMESTAMP where id = ?",
                currentAdminId(), number(app, "tenant_id").longValue());
        jdbcTemplate.update("update users set status = 0, updated_at = CURRENT_TIMESTAMP where id = ?",
                number(app, "owner_user_id").longValue());
        writeAdminAudit("TENANT_ONBOARDING_REJECT", "TENANT_ONBOARDING", String.valueOf(applicationId), "入驻审核驳回");
    }

    private void processTenantAuditApplication(int rowIndex) {
        Map<String, Object> row = tenantAuditRows().get(rowIndex);
        String id = text(row, "id");
        if (id.startsWith("TENANT-")) {
            Long tenantId = tenantIdFromAuditRow(rowIndex);
            jdbcTemplate.update("update tenants set status = 1, suspended_at = null, suspended_by = null, suspended_reason = null, updated_at = CURRENT_TIMESTAMP where id = ?", tenantId);
            return;
        }
        Long applicationId = applicationIdFromAuditRow(rowIndex);
        Map<String, Object> app = applicationRow(applicationId);
        long tenantId = number(app, "tenant_id").longValue();
        long ownerUserId = number(app, "owner_user_id").longValue();
        String planCode = planCodeFromTrial(text(app, "trial_plan"));
        Map<String, Object> plan = planRow(planCode);
        BigDecimal trialCredits = decimal(plan, "monthly_credits");
        LocalDateTime now = LocalDateTime.now();

        jdbcTemplate.update("""
                update tenants
                   set status = 1,
                       plan = ?,
                       suspended_at = null,
                       suspended_by = null,
                       suspended_reason = null,
                       notes = ?,
                       updated_at = CURRENT_TIMESTAMP
                 where id = ?
                """,
                planCode.toLowerCase(),
                "企业入驻审核通过，Owner 账号已激活",
                tenantId);
        jdbcTemplate.update("update users set status = 1, updated_at = CURRENT_TIMESTAMP where id = ?", ownerUserId);
        upsertTenantQuota(tenantId, plan);
        if (trialCredits.compareTo(BigDecimal.ZERO) > 0
                && count("select count(*) from billing_records where ref_id = ?", "ONBOARDING:" + applicationId) == 0) {
            jdbcTemplate.update("update tenants set credits = credits + ?, updated_at = CURRENT_TIMESTAMP where id = ?", trialCredits, tenantId);
            int updatedBalance = jdbcTemplate.update("""
                    update tenant_balances
                       set balance = balance + ?,
                           total_recharged = total_recharged + ?,
                           updated_at = CURRENT_TIMESTAMP
                     where tenant_id = ?
                    """, trialCredits, trialCredits, tenantId);
            if (updatedBalance == 0) {
                jdbcTemplate.update("""
                        insert into tenant_balances (id, tenant_id, balance, total_recharged, total_spent, version, updated_at)
                        values (?, ?, ?, ?, 0, 0, ?)
                        """, newBusinessId(), tenantId, trialCredits, trialCredits, now);
            }
            jdbcTemplate.update("""
                    insert into billing_records
                    (id, tenant_id, user_id, operation, credits, billing_mode, ref_id, created_by_admin_id,
                     modality, model_provider, model_id, model_name, unit_count, success, created_at)
                    values (?, ?, ?, 'recharge', ?, 'SOFT', ?, ?, 'RECHARGE', 'admin', 'onboarding_trial', '入驻试用额度', 1, 1, ?)
                    """,
                    newBusinessId(),
                    tenantId,
                    ownerUserId,
                    trialCredits,
                    "ONBOARDING:" + applicationId,
                    currentAdminId(),
                    now);
        }
        jdbcTemplate.update("""
                update tenant_onboarding_applications
                   set status = '已通过',
                       license_status = '已验真',
                       identity_status = '人证一致',
                       authorization_status = case when authorization_status = '待补件' then '无需授权' else authorization_status end,
                       risk_level = '低风险',
                       risk_reason = '主体、证照、法人身份与 Owner 关系审核通过',
                       review_note = coalesce(review_note, '审核通过并开通试用资源'),
                       reviewed_by = ?,
                       reviewed_at = CURRENT_TIMESTAMP,
                       updated_at = CURRENT_TIMESTAMP
                 where id = ?
                """, currentAdminId(), applicationId);
        writeAdminAudit("TENANT_ONBOARDING_APPROVE", "TENANT_ONBOARDING", String.valueOf(applicationId), "入驻审核通过并开通试用资源");
    }

    private Long applicationIdFromAuditRow(int rowIndex) {
        Map<String, Object> row = tenantAuditRows().get(rowIndex);
        String id = text(row, "id").replace("APP-", "");
        try {
            return Long.parseLong(id);
        } catch (NumberFormatException e) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "入驻申请编号格式错误");
        }
    }

    private Map<String, Object> applicationRow(Long applicationId) {
        return jdbcTemplate.query("""
                select *
                  from tenant_onboarding_applications
                 where id = ?
                """, rs -> {
            if (!rs.next()) {
                throw new BizException(ErrorCode.NOT_FOUND.getCode(), "入驻申请不存在");
            }
            Map<String, Object> row = new LinkedHashMap<>();
            for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) {
                row.put(rs.getMetaData().getColumnLabel(i), rs.getObject(i));
            }
            return row;
        }, applicationId);
    }

    private Map<String, Object> planRow(String planCode) {
        return jdbcTemplate.query("""
                select code, name, monthly_credits, max_members, max_projects, max_concurrent_tasks
                  from plans
                 where code = ?
                   and is_active = 1
                """, rs -> {
            if (!rs.next()) {
                throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "拟开通套餐不存在或已停用");
            }
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("code", rs.getString("code"));
            row.put("name", rs.getString("name"));
            row.put("monthly_credits", rs.getBigDecimal("monthly_credits"));
            row.put("max_members", rs.getInt("max_members"));
            row.put("max_projects", rs.getInt("max_projects"));
            row.put("max_concurrent_tasks", rs.getInt("max_concurrent_tasks"));
            return row;
        }, planCode);
    }

    private void upsertTenantQuota(long tenantId, Map<String, Object> plan) {
        int updated = jdbcTemplate.update("""
                update tenant_quotas
                   set max_members = ?,
                       max_projects = ?,
                       max_monthly_credits = ?,
                       max_concurrent_tasks = ?,
                       notes = ?,
                       updated_at = CURRENT_TIMESTAMP,
                       updated_by = ?
                 where tenant_id = ?
                """,
                number(plan, "max_members").intValue(),
                number(plan, "max_projects").intValue(),
                number(plan, "monthly_credits").intValue(),
                number(plan, "max_concurrent_tasks").intValue(),
                "入驻审核通过后按 " + text(plan, "name") + " 开通试用资源",
                currentAdminId(),
                tenantId);
        if (updated == 0) {
            jdbcTemplate.update("""
                    insert into tenant_quotas
                    (tenant_id, max_members, max_projects, max_monthly_credits, max_concurrent_tasks, notes, updated_at, updated_by)
                    values (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)
                    """,
                    tenantId,
                    number(plan, "max_members").intValue(),
                    number(plan, "max_projects").intValue(),
                    number(plan, "monthly_credits").intValue(),
                    number(plan, "max_concurrent_tasks").intValue(),
                    "入驻审核通过后按 " + text(plan, "name") + " 开通试用资源",
                    currentAdminId());
        }
    }

    private void writeAdminAudit(String action, String resourceType, String resourceId, String remark) {
        jdbcTemplate.update("""
                insert into admin_audit_logs
                (id, admin_user_id, admin_username, action, resource_type, resource_id, after_value, status, created_at)
                values (?, ?, '超级管理员', ?, ?, ?, ?, 'SUCCESS', CURRENT_TIMESTAMP)
                """,
                newBusinessId(),
                currentAdminId(),
                action,
                resourceType,
                resourceId,
                "{\"说明\":\"" + remark + "\"}");
    }

    private String statusFromReviewConclusion(String reviewConclusion, String fallback) {
        if (!StringUtils.hasText(reviewConclusion)) {
            return StringUtils.hasText(fallback) ? fallback : "资料待审";
        }
        if (reviewConclusion.contains("通过")) return "待开通";
        if (reviewConclusion.contains("补充")) return "待补件";
        if (reviewConclusion.contains("拒绝")) return "已驳回";
        return "人工复核";
    }

    private String riskFromStatus(String status) {
        if ("已驳回".equals(status)) return "高风险";
        if ("待补件".equals(status) || "人工复核".equals(status)) return "需人工确认";
        return "低风险";
    }

    private String valueOrCurrent(Map<String, Object> row, Map<String, Object> current, String key) {
        String value = requestText(row, key);
        return StringUtils.hasText(value) ? value : text(current, key);
    }

    private String legalPersonName(String value) {
        if (!StringUtils.hasText(value)) {
            return "";
        }
        int splitIndex = value.indexOf("/");
        return splitIndex >= 0 ? value.substring(0, splitIndex).trim() : value.trim();
    }

    private boolean hasRequiredMaterials(String licenseFile, String identityFile, String authorizationFile, boolean needsAuthorization) {
        return StringUtils.hasText(licenseFile)
                && StringUtils.hasText(identityFile)
                && (!needsAuthorization || StringUtils.hasText(authorizationFile));
    }

    private String materialRiskReason(String licenseFile, String identityFile, String authorizationFile, boolean needsAuthorization) {
        if (hasRequiredMaterials(licenseFile, identityFile, authorizationFile, needsAuthorization)) {
            return "证照、法人身份与授权材料已提交，待运营验真";
        }
        List<String> missing = new ArrayList<>();
        if (!StringUtils.hasText(licenseFile)) missing.add("营业执照");
        if (!StringUtils.hasText(identityFile)) missing.add("法人身份核验材料");
        if (needsAuthorization && !StringUtils.hasText(authorizationFile)) missing.add("经办人授权书");
        return "缺少：" + String.join("、", missing);
    }

    private String cleanCreditCode(String value) {
        return value == null ? "" : value.trim().toUpperCase();
    }

    private String planCodeFromTrial(String value) {
        if (!StringUtils.hasText(value)) {
            return "PRO";
        }
        String normalized = value.trim().toUpperCase();
        if (normalized.contains("TEAM") || normalized.contains("团队")) return "TEAM";
        if (normalized.contains("ENTERPRISE") || normalized.contains("企业")) return "ENTERPRISE";
        if (normalized.contains("FREE") || normalized.contains("免费")) return "FREE";
        return "PRO";
    }

    private String trialPlanLabel(String planCode) {
        return switch (planCode) {
            case "FREE" -> "免费版试用";
            case "TEAM" -> "团队版试用";
            case "ENTERPRISE" -> "企业版试用";
            default -> "专业版试用";
        };
    }

    private Map<String, Object> buildPermissionConfigPageData() {
        ensurePermissionConfigurationTables();
        List<Map<String, Object>> rows = permissionConfigRows();
        long enabled = rows.stream().filter(row -> "启用".equals(text(row, "status"))).count();
        long modelConfig = rows.stream().filter(row -> "模型配置".equals(text(row, "feature")) && "启用".equals(text(row, "status"))).count();
        long usageView = rows.stream().filter(row -> "用量统计".equals(text(row, "feature")) && "启用".equals(text(row, "status"))).count();
        long tenants = rows.stream().map(row -> text(row, "company")).filter(StringUtils::hasText).distinct().count();

        Map<String, Object> page = new LinkedHashMap<>();
        page.put("title", "权限配置");
        page.put("desc", "按企业、成员和前端功能分配权限，覆盖团队管理、用量统计、模型配置等企业配置入口");
        page.put("primary", "分配权限");
        page.put("allowCreate", true);
        page.put("allowEdit", true);
        page.put("allowDelete", true);
        page.put("allowProcess", true);
        page.put("workflow", List.of("选择企业", "选择成员", "勾选功能", "保存生效", "审计留痕"));
        page.put("metrics", List.of(
                metric("已配置权限", String.valueOf(enabled), "来自企业成员权限表"),
                metric("覆盖企业", String.valueOf(tenants), "已分配成员权限的企业"),
                metric("模型配置权限", String.valueOf(modelConfig), "可维护企业模型供应商"),
                metric("用量查看权限", String.valueOf(usageView), "可查看资源与模型消耗")
        ));
        page.put("filters", List.of("企业名称", "功能权限", "组织角色", "状态"));
        page.put("columns", List.of(
                column("company", "企业名称", 170, false),
                column("member", "成员账号", 170, false),
                column("role", "组织角色", 110, true),
                column("feature", "功能权限", 130, true),
                column("permissionScope", "数据范围", 160, false),
                column("status", "状态", 90, true),
                column("grantedAt", "配置时间", 150, false)
        ));
        page.put("rows", rows);
        page.put("cards", List.of(
                card("企业侧入口", "团队管理 / 用量统计 / 模型配置",
                        List.of("权限项与客户端企业配置入口保持一致",
                                "Owner 默认拥有企业全部数据与管理权限",
                                "管理员和成员按功能权限获得最小可用范围")),
                card("数据层级", "平台 > 企业租户 > 组织员工",
                        List.of("平台管理员配置企业可用权限",
                                "企业 Owner 可在客户端继续管理团队成员",
                                "权限变更写入配置表，可追溯授权来源")),
                card("真实数据源", "tenant_members + tenant_member_permissions",
                        List.of("成员来自真实租户组织关系",
                                "权限来自 permission_policies 固定可选项",
                                "停用权限只改变状态，不删除审计记录"))
        ));
        return page;
    }

    private List<Map<String, Object>> permissionConfigRows() {
        ensurePermissionConfigurationTables();
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
                select p.id,
                       p.tenant_id,
                       p.user_id,
                       p.policy_code,
                       p.permission_level,
                       p.data_scope,
                       p.grant_source,
                       p.status,
                       p.granted_at,
                       p.remark,
                       t.name as tenant_name,
                       t.slug,
                       u.username,
                       u.email,
                       u.phone,
                       tm.role,
                       tm.position,
                       pp.name as policy_name,
                       pp.category,
                       pp.scope,
                       pp.description
                  from tenant_member_permissions p
                  join permission_policies pp on pp.code = p.policy_code
                  join tenants t on t.id = p.tenant_id and t.deleted_at is null
                  join users u on u.id = p.user_id and u.deleted_at is null
                  left join tenant_members tm on tm.tenant_id = p.tenant_id and tm.user_id = p.user_id
                 order by t.name asc, u.username asc, pp.sort_order asc
                """);
        return rows.stream().map(row -> {
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("id", number(row, "id").longValue());
            next.put("tenantId", text(row, "tenant_id"));
            next.put("userId", text(row, "user_id"));
            next.put("policyCode", text(row, "policy_code"));
            next.put("company", text(row, "tenant_name"));
            next.put("member", text(row, "username"));
            next.put("account", accountText(row));
            next.put("role", roleLabel(text(row, "role")));
            next.put("position", text(row, "position"));
            next.put("feature", text(row, "policy_name"));
            next.put("category", text(row, "category"));
            next.put("policyScope", text(row, "scope"));
            next.put("description", text(row, "description"));
            next.put("permissionLevel", text(row, "permission_level"));
            next.put("permissionScope", text(row, "data_scope"));
            next.put("grantSource", text(row, "grant_source"));
            next.put("remark", text(row, "remark"));
            next.put("grantedAt", formatDateTime(time(row, "granted_at")));
            next.put("status", number(row, "status").intValue() == 1 ? "启用" : "停用");
            return next;
        }).toList();
    }

    private void ensurePermissionConfigurationTables() {
        jdbcTemplate.execute("""
                create table if not exists permission_policies
                (
                    code varchar(64) not null primary key,
                    name varchar(80) not null,
                    category varchar(40) not null,
                    scope varchar(80) not null,
                    description varchar(255) not null,
                    default_roles varchar(120),
                    status tinyint not null default 1,
                    sort_order int not null default 0,
                    created_at datetime(3) not null default current_timestamp(3),
                    updated_at datetime(3) not null default current_timestamp(3)
                )
                """);
        jdbcTemplate.execute("""
                create table if not exists tenant_member_permissions
                (
                    id bigint not null auto_increment primary key,
                    tenant_id bigint not null,
                    user_id bigint not null,
                    policy_code varchar(64) not null,
                    permission_level varchar(24) not null default '允许',
                    data_scope varchar(80) not null default '本企业',
                    grant_source varchar(80) not null default '运营配置',
                    status tinyint not null default 1,
                    granted_by bigint,
                    granted_at datetime(3) not null default current_timestamp(3),
                    revoked_at datetime(3),
                    remark varchar(255),
                    unique key uk_tenant_user_policy (tenant_id, user_id, policy_code)
                )
                """);
        seedPermissionPolicies();
        seedTenantMemberPermissions();
    }

    private void seedPermissionPolicies() {
        Object[][] policies = {
                {"TEAM_MANAGE", "团队管理", "企业配置", "企业成员与组织关系", "邀请、编辑、禁用成员并维护组织角色。", "owner,admin", 10},
                {"USAGE_VIEW", "用量统计", "企业配置", "资源流水与模型消耗", "查看企业用量、积分流水、模型消耗和导出统计。", "owner,admin,member", 20},
                {"MODEL_CONFIG", "模型配置", "企业配置", "模型供应商与默认模型", "维护企业接入的模型供应商、模型清单和默认模型。", "owner,admin", 30},
                {"PROJECT_MANAGE", "项目管理", "创作工具", "短剧项目与任务", "创建、编辑、删除项目并管理生成任务。", "owner,admin,member", 40},
                {"ASSET_MANAGE", "资产管理", "创作工具", "角色、场景、素材资产", "上传、复用和管理企业资产库。", "owner,admin,member", 50},
                {"BILLING_MANAGE", "费用管理", "费用", "套餐、订单与发票", "管理套餐、充值、发票与退款处理。", "owner,admin", 60},
                {"SECURITY_AUDIT", "安全审计", "备案", "操作记录与合规确认", "查看企业侧关键操作记录和政策确认状态。", "owner", 70}
        };
        for (Object[] policy : policies) {
            int updated = jdbcTemplate.update("""
                    update permission_policies
                       set name = ?, category = ?, scope = ?, description = ?, default_roles = ?, status = 1, sort_order = ?, updated_at = CURRENT_TIMESTAMP
                     where code = ?
                    """,
                    policy[1], policy[2], policy[3], policy[4], policy[5], policy[6], policy[0]);
            if (updated == 0) {
                jdbcTemplate.update("""
                        insert into permission_policies
                        (code, name, category, scope, description, default_roles, status, sort_order, created_at, updated_at)
                        values (?, ?, ?, ?, ?, ?, 1, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                        """,
                        policy[0], policy[1], policy[2], policy[3], policy[4], policy[5], policy[6]);
            }
        }
    }

    private void seedTenantMemberPermissions() {
        List<Map<String, Object>> members = jdbcTemplate.queryForList("""
                select tm.tenant_id, tm.user_id, tm.role
                  from tenant_members tm
                  join tenants t on t.id = tm.tenant_id and t.deleted_at is null
                  join users u on u.id = tm.user_id and u.deleted_at is null
                """);
        for (Map<String, Object> member : members) {
            long tenantId = number(member, "tenant_id").longValue();
            long userId = number(member, "user_id").longValue();
            String role = text(member, "role");
            for (String policyCode : defaultPolicyCodes(role)) {
                if (count("""
                        select count(*)
                          from tenant_member_permissions
                         where tenant_id = ? and user_id = ? and policy_code = ?
                        """, tenantId, userId, policyCode) > 0) {
                    continue;
                }
                jdbcTemplate.update("""
                        insert into tenant_member_permissions
                        (tenant_id, user_id, policy_code, permission_level, data_scope, grant_source, status, granted_by, granted_at, remark)
                        values (?, ?, ?, '允许', ?, '按组织角色初始化', 1, ?, CURRENT_TIMESTAMP, ?)
                        """,
                        tenantId,
                        userId,
                        policyCode,
                        dataScopeForRole(role),
                        currentAdminId(),
                        "由成员角色 " + roleLabel(role) + " 初始化");
            }
        }
    }

    private List<String> defaultPolicyCodes(String role) {
        if ("owner".equalsIgnoreCase(role)) {
            return List.of("TEAM_MANAGE", "USAGE_VIEW", "MODEL_CONFIG", "PROJECT_MANAGE", "ASSET_MANAGE", "BILLING_MANAGE", "SECURITY_AUDIT");
        }
        if ("admin".equalsIgnoreCase(role)) {
            return List.of("TEAM_MANAGE", "USAGE_VIEW", "MODEL_CONFIG", "PROJECT_MANAGE", "ASSET_MANAGE", "BILLING_MANAGE");
        }
        return List.of("USAGE_VIEW", "PROJECT_MANAGE", "ASSET_MANAGE");
    }

    private void addPermissionConfiguration(Map<String, Object> row) {
        upsertPermissionConfiguration(null, row);
    }

    private void updatePermissionConfiguration(Long id, Map<String, Object> row) {
        upsertPermissionConfiguration(id, row);
    }

    private void upsertPermissionConfiguration(Long id, Map<String, Object> row) {
        ensurePermissionConfigurationTables();
        long tenantId = resolveTenantId(row);
        long userId = resolveUserId(tenantId, row);
        String policyCode = resolvePolicyCode(row);
        String permissionLevel = StringUtils.hasText(requestText(row, "permissionLevel")) ? requestText(row, "permissionLevel") : "允许";
        String dataScope = StringUtils.hasText(requestText(row, "permissionScope")) ? requestText(row, "permissionScope") : dataScopeForRole(memberRole(tenantId, userId));
        String statusText = requestText(row, "status");
        int status = statusText.contains("停") || statusText.contains("禁") ? 0 : 1;
        String remark = requestText(row, "remark");
        if (id == null) {
            jdbcTemplate.update("""
                    insert into tenant_member_permissions
                    (tenant_id, user_id, policy_code, permission_level, data_scope, grant_source, status, granted_by, granted_at, remark)
                    values (?, ?, ?, ?, ?, '运营配置', ?, ?, CURRENT_TIMESTAMP, ?)
                    on duplicate key update
                        permission_level = values(permission_level),
                        data_scope = values(data_scope),
                        grant_source = values(grant_source),
                        status = values(status),
                        granted_by = values(granted_by),
                        granted_at = CURRENT_TIMESTAMP,
                        revoked_at = case when values(status) = 0 then CURRENT_TIMESTAMP else null end,
                        remark = values(remark)
                    """,
                    tenantId, userId, policyCode, permissionLevel, dataScope, status, currentAdminId(), remark);
        } else {
            jdbcTemplate.update("""
                    update tenant_member_permissions
                       set tenant_id = ?,
                           user_id = ?,
                           policy_code = ?,
                           permission_level = ?,
                           data_scope = ?,
                           grant_source = '运营配置',
                           status = ?,
                           granted_by = ?,
                           granted_at = CURRENT_TIMESTAMP,
                           revoked_at = case when ? = 0 then CURRENT_TIMESTAMP else null end,
                           remark = ?
                     where id = ?
                    """,
                    tenantId, userId, policyCode, permissionLevel, dataScope, status, currentAdminId(), status, remark, id);
        }
    }

    private Map<String, Object> buildModelConfigPageData() {
        ensureModelConfigurationTables();
        List<Map<String, Object>> rows = modelConfigRows();
        long providers = rows.size();
        long tenants = rows.stream().map(row -> text(row, "company")).filter(StringUtils::hasText).distinct().count();
        long defaults = count("select count(*) from tenant_model_defaults");
        long used = rows.stream().filter(row -> number(row, "callCount").longValue() > 0).count();

        Map<String, Object> page = new LinkedHashMap<>();
        page.put("title", "模型配置");
        page.put("desc", "按企业维护已接入模型供应商、可用模型和默认模型，数据与客户端企业配置同步");
        page.put("primary", "新增模型供应商");
        page.put("allowCreate", true);
        page.put("allowEdit", true);
        page.put("allowDelete", true);
        page.put("allowProcess", false);
        page.put("workflow", List.of("选择企业", "配置供应商", "勾选模型", "设置默认模型", "客户端生效"));
        page.put("metrics", List.of(
                metric("供应商配置", String.valueOf(providers), "来自 tenant_model_providers"),
                metric("覆盖企业", String.valueOf(tenants), "已配置模型的企业"),
                metric("默认模型", String.valueOf(defaults), "来自 tenant_model_defaults"),
                metric("产生调用", String.valueOf(used), "已有真实模型消耗记录")
        ));
        page.put("filters", List.of("企业名称", "模型能力", "供应商协议", "状态"));
        page.put("columns", List.of(
                column("company", "企业名称", 170, false),
                column("providerName", "供应商名称", 170, false),
                column("capability", "模型能力", 110, true),
                column("protocol", "接入协议", 130, true),
                column("models", "可用模型", 220, false),
                column("defaultModel", "默认模型", 160, false),
                column("callCount", "调用次数", 100, false),
                column("status", "状态", 90, true)
        ));
        page.put("rows", rows);
        page.put("cards", List.of(
                card("客户端同步", "企业配置 / 模型配置",
                        List.of("客户端只展示 tenant_model_providers 中已接入的供应商",
                                "默认模型从 tenant_model_defaults 读取",
                                "模型消耗从 billing_records 反向校验")),
                card("真实业务口径", "已接入模型优先",
                        List.of("优先读取企业模型供应商配置",
                                "缺配置但已有成功调用时，会从资源流水补齐上游配置",
                                "密钥只允许写入，不在列表中明文展示")),
                card("能力分类", "语言 / 图片 / 视频",
                        List.of("语言模型用于剧本、分镜与提示词处理",
                                "图片模型用于角色、场景、道具生成",
                                "视频模型用于图生视频与短剧片段生成"))
        ));
        return page;
    }

    private List<Map<String, Object>> modelConfigRows() {
        ensureModelConfigurationTables();
        if (count("select count(*) from tenant_model_providers") == 0) {
            syncModelProvidersFromBilling();
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
                select p.id,
                       p.tenant_id,
                       t.name as tenant_name,
                       p.name,
                       p.protocol,
                       p.capability,
                       p.base_url,
                       p.models_json,
                       p.updated_at
                  from tenant_model_providers p
                  join tenants t on t.id = p.tenant_id and t.deleted_at is null
                 order by t.name asc, p.capability asc, p.updated_at desc
                """);
        return rows.stream().map(row -> {
            long providerId = number(row, "id").longValue();
            long tenantId = number(row, "tenant_id").longValue();
            String protocol = text(row, "protocol");
            BigDecimal consumed = sum("""
                    select coalesce(sum(abs(credits)), 0)
                      from billing_records
                     where tenant_id = ?
                       and success = true
                       and model_provider = ?
                    """, tenantId, protocol);
            long callCount = count("""
                    select count(*)
                      from billing_records
                     where tenant_id = ?
                       and success = true
                       and model_provider = ?
                    """, tenantId, protocol);
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("id", providerId);
            next.put("tenantId", tenantId);
            next.put("company", text(row, "tenant_name"));
            next.put("providerName", text(row, "name"));
            next.put("protocol", protocolLabel(protocol));
            next.put("protocolCode", protocol);
            next.put("capability", capabilityLabel(text(row, "capability")));
            next.put("capabilityCode", text(row, "capability"));
            next.put("baseUrl", text(row, "base_url"));
            next.put("models", modelSummary(text(row, "models_json")));
            next.put("modelsJson", text(row, "models_json"));
            next.put("defaultModel", defaultModelText(providerId));
            next.put("callCount", String.valueOf(callCount));
            next.put("consumedCredits", formatCredits(consumed));
            next.put("updatedAt", formatDateTime(time(row, "updated_at")));
            next.put("status", "已接入");
            return next;
        }).toList();
    }

    private void ensureModelConfigurationTables() {
        jdbcTemplate.execute("""
                create table if not exists tenant_model_providers (
                    id bigint not null auto_increment primary key,
                    tenant_id bigint not null,
                    name varchar(128) not null,
                    protocol varchar(32) not null,
                    capability varchar(16) not null,
                    base_url varchar(512),
                    api_key varchar(512),
                    secret_key varchar(512),
                    models_json text,
                    created_at datetime(3) not null default current_timestamp(3),
                    updated_at datetime(3) not null default current_timestamp(3)
                )
                """);
        jdbcTemplate.execute("""
                create table if not exists tenant_model_defaults (
                    id bigint not null auto_increment primary key,
                    tenant_id bigint not null,
                    capability varchar(16) not null,
                    provider_id bigint not null,
                    model_id varchar(128) not null,
                    updated_at datetime(3) not null default current_timestamp(3),
                    unique key uk_tenant_cap (tenant_id, capability)
                )
                """);
    }

    private void addModelConfiguration(Map<String, Object> row) {
        ensureModelConfigurationTables();
        long tenantId = resolveTenantId(row);
        String protocol = normalizeProtocol(row);
        String capability = normalizeCapability(row);
        jdbcTemplate.update("""
                insert into tenant_model_providers
                (tenant_id, name, protocol, capability, base_url, api_key, secret_key, models_json, created_at, updated_at)
                values (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """,
                tenantId,
                requiredText(row, "providerName", "供应商名称不能为空"),
                protocol,
                capability,
                requestText(row, "baseUrl"),
                requestText(row, "apiKey"),
                requestText(row, "secretKey"),
                StringUtils.hasText(requestText(row, "modelsJson")) ? requestText(row, "modelsJson") : "[]");
    }

    private void updateModelConfiguration(Long id, Map<String, Object> row) {
        ensureModelConfigurationTables();
        List<Map<String, Object>> currentRows = jdbcTemplate.queryForList("select * from tenant_model_providers where id = ?", id);
        if (currentRows.isEmpty()) {
            throw new BizException(ErrorCode.NOT_FOUND.getCode(), "模型供应商不存在");
        }
        Map<String, Object> current = currentRows.get(0);
        long tenantId = StringUtils.hasText(requestText(row, "company")) ? resolveTenantId(row) : number(current, "tenant_id").longValue();
        String protocol = StringUtils.hasText(requestText(row, "protocol")) || StringUtils.hasText(requestText(row, "protocolCode"))
                ? normalizeProtocol(row)
                : text(current, "protocol");
        String capability = StringUtils.hasText(requestText(row, "capability")) || StringUtils.hasText(requestText(row, "capabilityCode"))
                ? normalizeCapability(row)
                : text(current, "capability");
        String apiKey = StringUtils.hasText(requestText(row, "apiKey")) ? requestText(row, "apiKey") : text(current, "api_key");
        String secretKey = StringUtils.hasText(requestText(row, "secretKey")) ? requestText(row, "secretKey") : text(current, "secret_key");
        jdbcTemplate.update("""
                update tenant_model_providers
                   set tenant_id = ?,
                       name = ?,
                       protocol = ?,
                       capability = ?,
                       base_url = ?,
                       api_key = ?,
                       secret_key = ?,
                       models_json = ?,
                       updated_at = CURRENT_TIMESTAMP
                 where id = ?
                """,
                tenantId,
                StringUtils.hasText(requestText(row, "providerName")) ? requestText(row, "providerName") : text(current, "name"),
                protocol,
                capability,
                StringUtils.hasText(requestText(row, "baseUrl")) ? requestText(row, "baseUrl") : text(current, "base_url"),
                apiKey,
                secretKey,
                StringUtils.hasText(requestText(row, "modelsJson")) ? requestText(row, "modelsJson") : text(current, "models_json"),
                id);
    }

    private void syncModelProvidersFromBilling() {
        List<Map<String, Object>> modelRows = jdbcTemplate.queryForList("""
                select tenant_id, model_provider, model_id, max(model_name) as model_name, max(modality) as modality
                  from billing_records
                 where success = true
                   and model_provider is not null
                   and model_id is not null
                 group by tenant_id, model_provider, model_id
                """);
        for (Map<String, Object> row : modelRows) {
            long tenantId = number(row, "tenant_id").longValue();
            String protocol = text(row, "model_provider");
            String modelId = text(row, "model_id");
            if (!StringUtils.hasText(protocol) || !StringUtils.hasText(modelId)) {
                continue;
            }
            if (count("""
                    select count(*)
                      from tenant_model_providers
                     where tenant_id = ? and protocol = ? and models_json like ?
                    """, tenantId, protocol, "%" + modelId + "%") > 0) {
                continue;
            }
            String modelName = StringUtils.hasText(text(row, "model_name")) ? text(row, "model_name") : modelId;
            jdbcTemplate.update("""
                    insert into tenant_model_providers
                    (tenant_id, name, protocol, capability, base_url, api_key, secret_key, models_json, created_at, updated_at)
                    values (?, ?, ?, ?, '', '', '', ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                    """,
                    tenantId,
                    modelName,
                    protocol,
                    capabilityFromModality(text(row, "modality")),
                    modelJson(modelId, modelName));
        }
    }

    private String uniqueTenantSlug(String creditCode) {
        String source = cleanCreditCode(creditCode).toLowerCase().replaceAll("[^a-z0-9-]", "");
        if (!StringUtils.hasText(source)) {
            source = "tenant";
        }
        String base = ("ent-" + source);
        if (base.length() > 42) {
            base = base.substring(0, 42);
        }
        String slug = base;
        int index = 1;
        while (count("select count(*) from tenants where slug = ?", slug) > 0) {
            slug = base + "-" + index++;
        }
        return slug;
    }

    private long newBusinessId() {
        return System.currentTimeMillis() * 1000 + Math.floorMod(UUID.randomUUID().getLeastSignificantBits(), 1000);
    }

    private Map<String, Object> buildOrdersInvoicesPageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        long rechargeOrders = count("""
                select count(*)
                  from billing_records
                 where credits > 0
                   and (operation = 'recharge' or modality = 'RECHARGE')
                """);
        BigDecimal totalCredits = sum("""
                select coalesce(sum(credits), 0)
                  from billing_records
                 where credits > 0
                   and (operation = 'recharge' or modality = 'RECHARGE')
                """);
        long todayOrders = count("""
                select count(*)
                  from billing_records
                 where credits > 0
                   and (operation = 'recharge' or modality = 'RECHARGE')
                   and created_at >= ?
                """, LocalDateTime.now().toLocalDate().atStartOfDay());
        page.put("title", "订单与发票");
        page.put("desc", "充值流水、资源入账、支付核验与发票状态统一对账");
        page.put("primary", "从计费记录生成");
        page.put("allowCreate", false);
        page.put("allowEdit", false);
        page.put("allowDelete", false);
        page.put("allowProcess", false);
        page.put("workflow", List.of("订单创建", "支付核验", "资源入账", "开票归档"));
        page.put("metrics", List.of(
                metric("充值订单", String.valueOf(rechargeOrders), "billing_records 真实充值流水"),
                metric("充值积分", formatCredits(totalCredits), "已入账积分合计"),
                metric("今日订单", String.valueOf(todayOrders), "今日充值/开通流水"),
                metric("待开票", String.valueOf(rechargeOrders), "未接入发票表前按订单待处理")
        ));
        page.put("filters", List.of("支付状态", "发票状态", "资源包", "企业名称"));
        page.put("columns", List.of(
                column("orderNo", "订单号", 190, false),
                column("company", "所属企业", 180, false),
                column("package", "资源包", 130, true),
                column("amount", "实付/入账积分", 150, false),
                column("invoice", "发票状态", 120, true),
                column("status", "支付状态", 120, true),
                column("createdAt", "创建时间", 160, false)
        ));
        page.put("rows", orderInvoiceRows());
        page.put("cards", List.of(
                card("数据来源", "billing_records + tenants",
                        List.of("订单号读取 ref_id，不再使用页面样例订单",
                                "资源包和企业名称从租户当前套餐同步",
                                "实付金额暂无独立订单金额表，当前以充值积分作为对账口径")),
                card("待补数据表", "invoice_requests / orders",
                        List.of("接入正式订单表后可替换 paid_amount",
                                "接入发票申请表后可区分待开具、已开具、已邮寄",
                                "现阶段所有状态均从真实充值流水派生"))
        ));
        return page;
    }

    private List<Map<String, Object>> orderInvoiceRows() {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
                select b.id, b.ref_id, b.credits, b.created_at, b.success,
                       t.name as tenant_name, t.plan
                  from billing_records b
                  left join tenants t on t.id = b.tenant_id
                 where b.credits > 0
                   and (b.operation = 'recharge' or b.modality = 'RECHARGE')
                 order by b.created_at desc
                 limit 50
                """);
        return rows.stream().map(row -> {
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("orderNo", StringUtils.hasText(text(row, "ref_id")) ? text(row, "ref_id") : "BR-" + text(row, "id"));
            next.put("company", StringUtils.hasText(text(row, "tenant_name")) ? text(row, "tenant_name") : "未知企业");
            next.put("package", planLabel(text(row, "plan")));
            next.put("amount", formatCredits(decimal(row, "credits")) + " 积分");
            next.put("invoice", "待开具");
            next.put("status", Boolean.FALSE.equals(row.get("success")) ? "异常" : "已支付");
            next.put("createdAt", formatDateTime(time(row, "created_at")));
            return next;
        }).toList();
    }

    private Map<String, Object> buildOrderOperationLogsPageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        List<Map<String, Object>> rows = orderOperationLogRows();
        long success = rows.stream().filter(row -> "成功".equals(row.get("status")) || "系统生成".equals(row.get("status"))).count();
        long failed = rows.stream().filter(row -> "失败".equals(row.get("status"))).count();
        long invoiceRelated = rows.stream().filter(row -> text(row, "invoiceStage").contains("开票")).count();
        long today = rows.stream().filter(row -> text(row, "operationTime").startsWith(LocalDateTime.now().toLocalDate().toString())).count();
        page.put("title", "订单操作记录");
        page.put("desc", "订单创建、支付核验、资源入账、发票待开和归档操作统一留痕");
        page.put("primary", "由业务自动记录");
        page.put("allowCreate", false);
        page.put("allowEdit", false);
        page.put("allowDelete", false);
        page.put("allowProcess", false);
        page.put("workflow", List.of("订单产生", "支付/入账", "发票处理", "操作留痕", "审计追溯"));
        page.put("metrics", List.of(
                metric("操作记录", String.valueOf(rows.size()), "来自 admin_audit_logs 与 billing_records"),
                metric("成功记录", String.valueOf(success), "已完成或系统生成"),
                metric("发票相关", String.valueOf(invoiceRelated), "待开票 / 开票 / 归档节点"),
                metric("今日新增", String.valueOf(today), "今日订单相关动作")
        ));
        page.put("filters", List.of("操作类型", "发票节点", "操作来源", "状态"));
        page.put("columns", List.of(
                column("operationTime", "操作时间", 160, false),
                column("orderNo", "订单号", 190, false),
                column("company", "所属企业", 180, false),
                column("operationType", "操作类型", 140, true),
                column("invoiceStage", "发票节点", 130, true),
                column("operator", "操作人", 130, true),
                column("source", "操作来源", 140, true),
                column("status", "状态", 100, true),
                column("evidence", "关联凭证", 220, false),
                column("remark", "说明", 260, false)
        ));
        page.put("rows", rows);
        page.put("cards", List.of(
                card("真实数据源", "admin_audit_logs + billing_records",
                        List.of("运营写操作优先读取审计日志",
                                "订单创建与入账节点从真实充值流水补齐",
                                "发票节点暂按订单待开票状态派生，接入 invoice_requests 后可精确到开具/邮寄")),
                card("闭环规则", "订单 -> 发票 -> 审计",
                        List.of("每条订单应能追溯创建、支付核验、资源入账和发票状态",
                                "失败操作保留错误信息，便于财务复核",
                                "只读展示，不允许手工改写事实记录"))
        ));
        return page;
    }

    private List<Map<String, Object>> orderOperationLogRows() {
        List<Map<String, Object>> rows = new ArrayList<>();
        List<Map<String, Object>> audits = jdbcTemplate.queryForList("""
                select id, action, resource_type, resource_id, admin_username, status, error_message, created_at
                  from admin_audit_logs
                 where action in ('TENANT_RECHARGE', 'BILLING_COST_UPDATE')
                    or resource_type in ('ORDER', 'INVOICE', 'ORDER_INVOICE', 'BILLING_RECORD')
                 order by created_at desc
                 limit 80
                """);
        for (Map<String, Object> audit : audits) {
            String action = text(audit, "action");
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("operationTime", formatDateTime(time(audit, "created_at")));
            next.put("orderNo", StringUtils.hasText(text(audit, "resource_id")) ? text(audit, "resource_id") : "AUDIT-" + text(audit, "id"));
            next.put("company", "按审计资源追溯");
            next.put("operationType", orderActionLabel(action));
            next.put("invoiceStage", invoiceStageForAction(action));
            next.put("operator", StringUtils.hasText(text(audit, "admin_username")) ? text(audit, "admin_username") : "系统");
            next.put("source", "操作审计日志");
            next.put("status", "SUCCESS".equalsIgnoreCase(text(audit, "status")) ? "成功" : "失败");
            next.put("evidence", text(audit, "resource_type") + " / " + text(audit, "resource_id"));
            next.put("remark", StringUtils.hasText(text(audit, "error_message")) ? text(audit, "error_message") : "运营写操作已自动留痕");
            rows.add(next);
        }

        List<Map<String, Object>> billings = jdbcTemplate.queryForList("""
                select b.id, b.ref_id, b.credits, b.created_at, b.success,
                       t.name as tenant_name, t.plan
                  from billing_records b
                  left join tenants t on t.id = b.tenant_id
                 where b.credits > 0
                   and (b.operation = 'recharge' or b.modality = 'RECHARGE')
                 order by b.created_at desc
                 limit 80
                """);
        for (Map<String, Object> billing : billings) {
            String orderNo = StringUtils.hasText(text(billing, "ref_id")) ? text(billing, "ref_id") : "BR-" + text(billing, "id");
            Map<String, Object> create = new LinkedHashMap<>();
            create.put("operationTime", formatDateTime(time(billing, "created_at")));
            create.put("orderNo", orderNo);
            create.put("company", StringUtils.hasText(text(billing, "tenant_name")) ? text(billing, "tenant_name") : "未知企业");
            create.put("operationType", "订单入账生成");
            create.put("invoiceStage", "待开票");
            create.put("operator", "系统");
            create.put("source", "计费记录");
            create.put("status", Boolean.FALSE.equals(billing.get("success")) ? "失败" : "系统生成");
            create.put("evidence", formatCredits(decimal(billing, "credits")) + " 积分 / " + planLabel(text(billing, "plan")));
            create.put("remark", "由真实充值流水生成订单和发票待处理节点");
            rows.add(create);
        }

        return rows.stream()
                .sorted((left, right) -> text(right, "operationTime").compareTo(text(left, "operationTime")))
                .limit(100)
                .toList();
    }

    private String orderActionLabel(String action) {
        return switch (action) {
            case "TENANT_RECHARGE" -> "手动充值入账";
            case "BILLING_COST_UPDATE" -> "计费规则调整";
            default -> StringUtils.hasText(action) ? action : "订单操作";
        };
    }

    private String invoiceStageForAction(String action) {
        return switch (action) {
            case "TENANT_RECHARGE" -> "待开票";
            case "BILLING_COST_UPDATE" -> "计费规则";
            default -> "审计归档";
        };
    }

    private Map<String, Object> buildWarningsPageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        Map<String, Object> settings = getPageSettings("warnings");
        List<Map<String, Object>> rows = warningRows(settings);
        long severe = rows.stream().filter(row -> "严重".equals(row.get("level"))).count();
        long escalated = rows.stream().filter(row -> "已升级".equals(row.get("status"))).count();
        page.put("title", "运营预警");
        page.put("desc", "资源、任务、审核、工单、发票、内容安全与到期风险统一扫描和闭环处理");
        page.put("primary", "自动扫描生成");
        page.put("allowCreate", false);
        page.put("allowEdit", false);
        page.put("allowDelete", false);
        page.put("allowProcess", false);
        page.put("workflow", List.of("规则扫描", "阈值命中", "通知负责人", "升级处理", "复盘归档"));
        page.put("metrics", List.of(
                metric("待处理预警", String.valueOf(rows.size()), "从客户端业务表实时扫描"),
                metric("严重预警", String.valueOf(severe), "任务失败/内容风险/资源严重不足"),
                metric("今日新增", String.valueOf(rows.size()), "按当前参数命中"),
                metric("已升级", String.valueOf(escalated), "需跨团队处理")
        ));
        page.put("filters", List.of("预警类型", "风险等级", "处理状态", "负责人"));
        page.put("columns", List.of(
                column("company", "企业名称", 170, false),
                column("type", "预警类型", 140, true),
                column("level", "风险等级", 110, true),
                column("reason", "触发原因", 260, false),
                column("triggerValue", "触发值", 120, false),
                column("threshold", "触发阈值", 120, false),
                column("owner", "负责人", 120, true),
                column("channels", "提醒渠道", 160, false),
                column("nextAction", "下一步动作", 220, false),
                column("status", "处理状态", 120, true)
        ));
        page.put("rows", rows);
        page.put("warningSettings", settings);
        page.put("cards", List.of(
                card("同步数据源", "tenants / users / projects / billing_records / ai_tasks",
                        List.of("余额、套餐、成员、项目、资源流水与任务失败均来自客户端同库",
                                "预警参数来自 system_settings.warnings.rules",
                                "无命中风险时列表会自然为空，不再展示固定样例")),
                card("处置建议", "资源 / 任务 / 客服 / 财务",
                        List.of("资源不足推送续费或充值包",
                                "失败率异常转技术值班复查模型与素材",
                                "沉睡客户进入客服回访和案例激活"))
        ));
        return page;
    }

    private List<Map<String, Object>> warningRows(Map<String, Object> settings) {
        List<Map<String, Object>> rows = new ArrayList<>();
        int lowPercent = intSetting(settings, "creditLowPercent", 20);
        int criticalPercent = intSetting(settings, "creditCriticalPercent", 10);
        int dormantDays = intSetting(settings, "dormantDays", 30);
        int taskFailRate = intSetting(settings, "taskFailRatePercent", 15);
        LocalDateTime dormantCutoff = LocalDateTime.now().minusDays(dormantDays);

        List<Map<String, Object>> tenants = jdbcTemplate.queryForList("""
                select t.id, t.name, t.plan, t.status, t.updated_at,
                       coalesce(tb.balance, t.credits, 0) as balance,
                       coalesce(tq.max_monthly_credits, 0) as monthly_credits,
                       max(u.last_login_at) as last_login_at
                  from tenants t
                  left join tenant_balances tb on tb.tenant_id = t.id
                  left join tenant_quotas tq on tq.tenant_id = t.id
                  left join users u on u.tenant_id = t.id and u.deleted_at is null
                 where t.deleted_at is null
                 group by t.id, t.name, t.plan, t.status, t.updated_at, tb.balance, t.credits, tq.max_monthly_credits
                 order by t.updated_at desc
                """);
        for (Map<String, Object> tenant : tenants) {
            BigDecimal balance = decimal(tenant, "balance");
            BigDecimal monthly = decimal(tenant, "monthly_credits");
            if (monthly.compareTo(BigDecimal.ZERO) > 0) {
                BigDecimal percent = balance.multiply(BigDecimal.valueOf(100)).divide(monthly, 1, RoundingMode.HALF_UP);
                if (percent.compareTo(BigDecimal.valueOf(lowPercent)) < 0) {
                    rows.add(warningRow(
                            "WARN-CREDIT-" + text(tenant, "id"),
                            text(tenant, "name"),
                            "资源不足",
                            percent.compareTo(BigDecimal.valueOf(criticalPercent)) < 0 ? "严重" : "高",
                            "当前可用积分低于资源预警阈值，可能影响客户端继续生成任务",
                            percent.stripTrailingZeros().toPlainString() + "%",
                            "<" + lowPercent + "%",
                            "财务值班",
                            "站内信、企业微信",
                            "核对余额并推送充值/续费方案",
                            percent.compareTo(BigDecimal.valueOf(criticalPercent)) < 0 ? "已升级" : "待处理"));
                }
            }
            LocalDateTime lastLogin = time(tenant, "last_login_at");
            if (lastLogin == null || lastLogin.isBefore(dormantCutoff)) {
                rows.add(warningRow(
                        "WARN-DORMANT-" + text(tenant, "id"),
                        text(tenant, "name"),
                        "沉睡客户",
                        "中",
                        "超过沉睡天数未登录，需确认客户端项目是否停滞",
                        lastLogin == null ? "从未登录" : formatDateTime(lastLogin),
                        ">" + dormantDays + " 天",
                        "客服主管",
                        "邮件、企业微信",
                        "进入客服回访队列并推送项目案例",
                        "待处理"));
            }
        }

        rows.addAll(taskFailureWarnings(taskFailRate));
        return rows.stream().limit(50).toList();
    }

    private List<Map<String, Object>> taskFailureWarnings(int taskFailRate) {
        List<Map<String, Object>> rows = new ArrayList<>();
        List<Map<String, Object>> stats = jdbcTemplate.queryForList("""
                select t.id, t.name,
                       count(b.id) as total_count,
                       sum(case when b.success = false then 1 else 0 end) as fail_count
                  from tenants t
                  join billing_records b on b.tenant_id = t.id
                 where t.deleted_at is null
                   and (b.operation <> 'recharge' or b.operation is null)
                   and (b.modality <> 'RECHARGE' or b.modality is null)
                 group by t.id, t.name
                """);
        for (Map<String, Object> stat : stats) {
            long total = number(stat, "total_count").longValue();
            long fail = number(stat, "fail_count").longValue();
            if (total <= 0) {
                continue;
            }
            BigDecimal rate = BigDecimal.valueOf(fail * 100).divide(BigDecimal.valueOf(total), 1, RoundingMode.HALF_UP);
            if (rate.compareTo(BigDecimal.valueOf(taskFailRate)) > 0) {
                rows.add(warningRow(
                        "WARN-FAIL-" + text(stat, "id"),
                        text(stat, "name"),
                        "任务失败率",
                        rate.compareTo(BigDecimal.valueOf(taskFailRate * 2L)) > 0 ? "严重" : "高",
                        "客户端模型调用失败率超过任务健康阈值",
                        rate.stripTrailingZeros().toPlainString() + "%",
                        ">" + taskFailRate + "%",
                        "技术值班",
                        "站内信、短信、企业微信",
                        "复查模型供应商、素材格式与失败任务日志",
                        "已升级"));
            }
        }
        return rows;
    }

    private Map<String, Object> buildGlobalSettingsPageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        List<Map<String, Object>> rows = globalSettingRows();
        long enabled = rows.stream().filter(row -> "启用".equals(row.get("status"))).count();
        long warningCount = rows.stream().filter(row -> "预警".equals(row.get("group"))).count();
        long grayCount = rows.stream().filter(row -> "灰度".equals(row.get("group"))).count();
        long enabledGray = rows.stream().filter(row -> "灰度".equals(row.get("group")) && "启用".equals(row.get("status"))).count();
        String lowPercent = rows.stream()
                .filter(row -> "platform.warning.credit_low_percent".equals(row.get("configKey")))
                .map(row -> text(row, "value"))
                .findFirst()
                .orElse("20%");
        String expireDays = rows.stream()
                .filter(row -> "platform.warning.expire_remind_days".equals(row.get("configKey")))
                .map(row -> text(row, "value"))
                .findFirst()
                .orElse("7 天");
        page.put("title", "全局参数配置");
        page.put("desc", "预警阈值、灰度开关、积分单价与平台安全策略从 system_settings 统一读取和生效");
        page.put("primary", "保存配置");
        page.put("workflow", List.of("配置编辑", "风险校验", "保存生效", "审计留痕"));
        page.put("metrics", List.of(
                metric("预警阈值", lowPercent, "资源不足触发线"),
                metric("到期提醒", expireDays, "来自平台预警参数"),
                metric("灰度开关", String.valueOf(grayCount), enabledGray + " 个已启用"),
                metric("生效配置", String.valueOf(enabled), "system_settings 启用项")
        ));
        page.put("filters", List.of("配置分组", "状态", "维护人", "生效范围"));
        page.put("columns", List.of(
                column("key", "配置项", 220, false),
                column("group", "分组", 110, true),
                column("value", "当前值", 160, false),
                column("scope", "生效范围", 130, true),
                column("owner", "维护人", 120, true),
                column("status", "状态", 100, true)
        ));
        page.put("rows", rows);
        page.put("cards", List.of(
                card("真实数据源", "system_settings",
                        List.of("配置中文名、分组、类型、生效范围和当前值均从数据库读取",
                                "列表只展示 platform.* 平台参数，避免把页面 JSON 配置误当业务参数",
                                "新增/编辑/停用/生效会回写 system_settings")),
                card("安全要求", "高危配置二次确认",
                        List.of("影响资源、计费、灰度、风控的配置必须填写变更原因",
                                "停用通过软状态处理，便于审计追溯",
                                "预警类参数与运营预警表的阈值口径保持一致"))
        ));
        page.put("logicRules", List.of(
                rule("资源不足阈值", "当租户余额占月额度比例低于该值时进入运营预警。", "剩余比例 = tenant_balances.balance / tenant_quotas.max_monthly_credits * 100%", "财务运营", "可调整百分比", "建议同步通知客服和商务。"),
                rule("灰度开关", "控制新功能面向哪些租户或套餐开放。", "命中范围 = setting_group=灰度 且 status=启用 且 scope 匹配租户属性", "产品运营", "可调整范围和状态", "灰度变更建议保留白名单说明。"),
                rule("平台安全策略", "约束强制阅读、内容拦截、登录和风险校验等全局策略。", "启用策略按 setting_key 读取，客户端与运营端共用同一配置源。", "安全运营", "需管理员确认", "高危策略变更会进入审计日志。")
        ));
        return page;
    }

    private List<Map<String, Object>> globalSettingRows() {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
                select s.id, s.setting_key, s.setting_name, s.setting_group, s.setting_value, s.value_type,
                       s.scope, s.status, s.effective_at, s.change_reason, s.description, s.updated_at,
                       coalesce(a.display_name, '超级管理员') as owner
                  from system_settings s
                  left join admin_users a on a.id = s.updated_by
                 where s.setting_key like 'platform.%'
                 order by
                       case s.setting_group
                         when '预警' then 1
                         when '计费' then 2
                         when '灰度' then 3
                         when '安全' then 4
                         else 9
                       end,
                       s.setting_key asc
                """);
        return rows.stream().map(row -> {
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("id", text(row, "id"));
            next.put("configKey", text(row, "setting_key"));
            next.put("key", StringUtils.hasText(text(row, "setting_name")) ? text(row, "setting_name") : text(row, "setting_key"));
            next.put("group", text(row, "setting_group"));
            next.put("value", displaySettingValue(row));
            next.put("valueType", text(row, "value_type"));
            next.put("scope", text(row, "scope"));
            next.put("effectiveAt", formatDateTime(time(row, "effective_at")));
            next.put("changeReason", text(row, "change_reason"));
            next.put("description", text(row, "description"));
            next.put("owner", text(row, "owner"));
            next.put("status", text(row, "status"));
            next.put("updatedAt", formatDateTime(time(row, "updated_at")));
            return next;
        }).toList();
    }

    private String displaySettingValue(Map<String, Object> row) {
        String value = text(row, "setting_value");
        String type = text(row, "value_type");
        if (!StringUtils.hasText(value)) {
            return "";
        }
        if ("percent".equals(type) && !value.endsWith("%")) {
            return value + "%";
        }
        if ("days".equals(type) && !value.endsWith("天")) {
            return value + " 天";
        }
        if ("hours".equals(type) && !value.endsWith("小时")) {
            return value + " 小时";
        }
        if ("boolean".equals(type)) {
            return "true".equalsIgnoreCase(value) || "1".equals(value) || "启用".equals(value) ? "启用" : "停用";
        }
        return value;
    }

    private void addGlobalSetting(Map<String, Object> row) {
        String configKey = requiredText(row, "configKey", "参数编码不能为空");
        if (!configKey.startsWith("platform.")) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "平台参数编码必须以 platform. 开头");
        }
        jdbcTemplate.update("""
                insert into system_settings
                (id, setting_key, setting_name, setting_group, setting_value, value_type, scope, status,
                 effective_at, change_reason, description, updated_at, updated_by)
                values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)
                """,
                System.currentTimeMillis(),
                configKey,
                requiredText(row, "key", "配置项名称不能为空"),
                requestText(row, "group"),
                normalizeSettingValue(requestText(row, "value")),
                StringUtils.hasText(requestText(row, "valueType")) ? requestText(row, "valueType") : "text",
                StringUtils.hasText(requestText(row, "scope")) ? requestText(row, "scope") : "全平台",
                StringUtils.hasText(requestText(row, "status")) ? requestText(row, "status") : "待生效",
                parseDateTime(requestText(row, "effectiveAt")),
                requestText(row, "changeReason"),
                requestText(row, "description"),
                currentAdminId());
    }

    private void updateGlobalSetting(Long id, Map<String, Object> row) {
        jdbcTemplate.update("""
                update system_settings
                   set setting_name = ?,
                       setting_group = ?,
                       setting_value = ?,
                       value_type = ?,
                       scope = ?,
                       status = ?,
                       effective_at = ?,
                       change_reason = ?,
                       description = ?,
                       updated_at = CURRENT_TIMESTAMP,
                       updated_by = ?
                 where id = ?
                """,
                requiredText(row, "key", "配置项名称不能为空"),
                requestText(row, "group"),
                normalizeSettingValue(requestText(row, "value")),
                StringUtils.hasText(requestText(row, "valueType")) ? requestText(row, "valueType") : "text",
                StringUtils.hasText(requestText(row, "scope")) ? requestText(row, "scope") : "全平台",
                StringUtils.hasText(requestText(row, "status")) ? requestText(row, "status") : "待生效",
                parseDateTime(requestText(row, "effectiveAt")),
                requestText(row, "changeReason"),
                requestText(row, "description"),
                currentAdminId(),
                id);
    }

    private String normalizeSettingValue(String value) {
        if (!StringUtils.hasText(value)) {
            return "";
        }
        return value.replace("天", "").replace("小时", "").replace("%", "").trim();
    }

    private long currentAdminId() {
        try {
            return StpAdminUtil.getLoginIdAsLong();
        } catch (Exception e) {
            return 1L;
        }
    }

    private Map<String, Object> buildTicketsPageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        List<Map<String, Object>> rows = ticketRows();
        long unassigned = rows.stream().filter(row -> "待分配".equals(row.get("status"))).count();
        long processing = rows.stream().filter(row -> "处理中".equals(row.get("status"))).count();
        long todayResolved = rows.stream().filter(row -> {
            String status = text(row, "status");
            String resolvedAt = text(row, "resolvedAt");
            return "已解决".equals(status) && resolvedAt.startsWith(LocalDateTime.now().toLocalDate().toString());
        }).count();
        long timeoutRisk = rows.stream().filter(row -> "是".equals(row.get("timeoutRisk"))).count();
        long urgent = rows.stream().filter(row -> "紧急".equals(row.get("priority"))).count();
        long resolved = rows.stream().filter(row -> "已解决".equals(row.get("status")) || "已关闭".equals(row.get("status"))).count();
        long total = rows.size();
        BigDecimal resolveRate = ratio(resolved, total);

        page.put("title", "工单工作台");
        page.put("desc", "企业咨询、问题反馈、转派与 SLA 跟踪");
        page.put("primary", "新建工单");
        page.put("allowCreate", true);
        page.put("allowEdit", true);
        page.put("allowDelete", true);
        page.put("allowProcess", true);
        page.put("workflow", List.of("待分配", "处理中", "已解决", "已关闭"));
        page.put("metrics", List.of(
                metric("待分配", String.valueOf(unassigned), "紧急 " + urgent + " 条"),
                metric("处理中", String.valueOf(processing), "来自 customer_tickets"),
                metric("今日解决", String.valueOf(todayResolved), "解决率 " + percent(resolveRate)),
                metric("超时风险", String.valueOf(timeoutRisk), "按 SLA 截止时间扫描")
        ));
        page.put("filters", List.of("工单状态", "优先级", "工单分类", "处理人"));
        page.put("columns", List.of(
                column("ticketNo", "工单号", 160, false),
                column("company", "企业", 170, false),
                column("category", "分类", 110, true),
                column("priority", "优先级", 100, true),
                column("assignee", "处理人", 120, true),
                column("status", "状态", 120, true),
                column("subject", "问题主题", 220, false),
                column("timeoutRisk", "超时风险", 110, true),
                column("dueAt", "SLA 截止", 150, false)
        ));
        page.put("rows", rows);
        page.put("cards", List.of(
                card("真实数据源", "customer_tickets + tenants + users",
                        List.of("工单主体读取 customer_tickets",
                                "企业名称、套餐和账号归属从 tenants / users 联动",
                                "任务故障类工单可关联 ai_tasks 或 billing_records 的任务编号")),
                card("SLA 口径", "紧急 2h / 加急 4h / 普通 24h",
                        List.of("超时风险由 due_at 与当前时间实时判断",
                                "首响、解决时间用于客服绩效看板",
                                "处理人为空时显示待分配并进入分配队列")),
                card("业务闭环", "工单 -> 话术 -> 预警",
                        List.of("资源、生成、账号、发票等分类对应话术库场景",
                                "超时工单同步运营预警表",
                                "解决/关闭后仍保留来源、描述和处理结论"))
        ));
        return page;
    }

    private List<Map<String, Object>> ticketRows() {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
                select ct.id, ct.ticket_no, ct.tenant_id, ct.requester_user_id, ct.category, ct.priority,
                       ct.status, ct.assignee, ct.source, ct.subject, ct.description, ct.related_ref,
                       ct.first_response_at, ct.resolved_at, ct.due_at, ct.created_at, ct.updated_at,
                       t.name as tenant_name, t.plan, u.username as requester_name, u.email as requester_email
                  from customer_tickets ct
                  left join tenants t on t.id = ct.tenant_id
                  left join users u on u.id = ct.requester_user_id
                 order by
                       case ct.status
                         when '待分配' then 1
                         when '处理中' then 2
                         when '待客户确认' then 3
                         when '已解决' then 4
                         else 5
                       end,
                       ct.updated_at desc
                """);
        return rows.stream().map(row -> {
            LocalDateTime dueAt = time(row, "due_at");
            LocalDateTime createdAt = time(row, "created_at");
            LocalDateTime firstResponseAt = time(row, "first_response_at");
            LocalDateTime resolvedAt = time(row, "resolved_at");
            String status = text(row, "status");
            boolean timeoutRisk = dueAt != null
                    && dueAt.isBefore(LocalDateTime.now())
                    && !List.of("已解决", "已关闭").contains(status);
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("id", text(row, "id"));
            next.put("ticketNo", text(row, "ticket_no"));
            next.put("company", StringUtils.hasText(text(row, "tenant_name")) ? text(row, "tenant_name") : "未知企业");
            next.put("category", text(row, "category"));
            next.put("priority", text(row, "priority"));
            next.put("assignee", StringUtils.hasText(text(row, "assignee")) ? text(row, "assignee") : "待分配");
            next.put("status", status);
            next.put("source", text(row, "source"));
            next.put("subject", text(row, "subject"));
            next.put("description", text(row, "description"));
            next.put("requester", StringUtils.hasText(text(row, "requester_name")) ? text(row, "requester_name") : text(row, "requester_email"));
            next.put("relatedRef", text(row, "related_ref"));
            next.put("plan", planLabel(text(row, "plan")));
            next.put("firstResponse", durationText(createdAt, firstResponseAt));
            next.put("resolvedAt", formatDateTime(resolvedAt));
            next.put("dueAt", formatDateTime(dueAt));
            next.put("timeoutRisk", timeoutRisk ? "是" : "否");
            next.put("createdAt", formatDateTime(createdAt));
            return next;
        }).toList();
    }

    private Long tenantIdFromAuditRow(int rowIndex) {
        Map<String, Object> row = tenantAuditRows().get(rowIndex);
        String id = text(row, "id").replace("TENANT-", "");
        try {
            return Long.parseLong(id);
        } catch (NumberFormatException e) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "租户编号格式错误");
        }
    }

    private void addTicket(Map<String, Object> row) {
        Long tenantId = firstTenantId();
        jdbcTemplate.update("""
                insert into customer_tickets
                (id, ticket_no, tenant_id, category, priority, status, assignee, source, subject, description, related_ref,
                 due_at, created_at, updated_at)
                values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """,
                System.currentTimeMillis(),
                "TK" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()),
                tenantId,
                StringUtils.hasText(requestText(row, "category")) ? requestText(row, "category") : "咨询",
                StringUtils.hasText(requestText(row, "priority")) ? requestText(row, "priority") : "普通",
                StringUtils.hasText(requestText(row, "status")) ? requestText(row, "status") : "待分配",
                requestText(row, "assignee"),
                StringUtils.hasText(requestText(row, "source")) ? requestText(row, "source") : "运营代提",
                requiredText(row, "subject", "工单主题不能为空"),
                requestText(row, "description"),
                requestText(row, "relatedRef"),
                LocalDateTime.now().plusHours("紧急".equals(requestText(row, "priority")) ? 2 : "加急".equals(requestText(row, "priority")) ? 4 : 24));
    }

    private void updateTicket(Long id, Map<String, Object> row) {
        jdbcTemplate.update("""
                update customer_tickets
                   set category = ?,
                       priority = ?,
                       status = ?,
                       assignee = ?,
                       source = ?,
                       subject = ?,
                       description = ?,
                       related_ref = ?,
                       due_at = ?,
                       updated_at = CURRENT_TIMESTAMP
                 where id = ?
                """,
                StringUtils.hasText(requestText(row, "category")) ? requestText(row, "category") : "咨询",
                StringUtils.hasText(requestText(row, "priority")) ? requestText(row, "priority") : "普通",
                StringUtils.hasText(requestText(row, "status")) ? requestText(row, "status") : "待分配",
                requestText(row, "assignee"),
                StringUtils.hasText(requestText(row, "source")) ? requestText(row, "source") : "运营代提",
                requiredText(row, "subject", "工单主题不能为空"),
                requestText(row, "description"),
                requestText(row, "relatedRef"),
                parseDateTime(requestText(row, "dueAt")),
                id);
    }

    private Long firstTenantId() {
        Long tenantId = jdbcTemplate.query(
                "select id from tenants where deleted_at is null order by created_at asc limit 1",
                rs -> rs.next() ? rs.getLong(1) : null);
        if (tenantId == null) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), "请先创建企业租户后再新增工单");
        }
        return tenantId;
    }

    private Map<String, Object> buildSupportPerformancePageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        List<Map<String, Object>> rows = supportPerformanceRows();
        long handled = rows.stream().mapToLong(row -> number(row, "handled").longValue()).sum();
        BigDecimal avgResolution = rows.isEmpty()
                ? BigDecimal.ZERO
                : rows.stream()
                .map(row -> text(row, "resolution").replace("%", ""))
                .map(value -> StringUtils.hasText(value) ? new BigDecimal(value) : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add)
                .divide(BigDecimal.valueOf(rows.size()), 1, RoundingMode.HALF_UP);
        long riskCount = rows.stream().filter(row -> !"低".equals(row.get("risk"))).count();
        page.put("title", "客服绩效看板");
        page.put("desc", "响应时效、处理量、解决率与超时风险统计");
        page.put("primary", "从工单统计生成");
        page.put("allowCreate", false);
        page.put("allowEdit", false);
        page.put("allowDelete", false);
        page.put("allowProcess", false);
        page.put("workflow", List.of("工单进入", "首响统计", "解决统计", "SLA 复盘"));
        page.put("metrics", List.of(
                metric("处理总量", String.valueOf(handled), "来自 customer_tickets"),
                metric("平均解决率", percent(avgResolution), "按客服人员聚合"),
                metric("风险人员", String.valueOf(riskCount), "存在中/高超时风险"),
                metric("统计范围", "近 30 天", "以工单更新时间为准")
        ));
        page.put("filters", List.of("客服人员", "时间范围", "工单分类", "优先级"));
        page.put("columns", List.of(
                column("agent", "客服", 120, false),
                column("handled", "处理量", 110, false),
                column("firstResponse", "平均首响", 130, false),
                column("resolution", "解决率", 110, false),
                column("timeoutCount", "超时工单", 110, false),
                column("risk", "超时风险", 120, true)
        ));
        page.put("rows", rows);
        page.put("cards", List.of(
                card("真实数据源", "customer_tickets",
                        List.of("客服人员、处理量、首响和解决率均从工单表聚合",
                                "处理人为空的工单归入待分配队列",
                                "SLA 风险按 due_at 和工单状态实时判断")),
                card("绩效口径", "处理量 / 首响 / 解决率",
                        List.of("平均首响 = first_response_at - created_at",
                                "解决率 = 已解决或已关闭工单 / 总处理工单",
                                "超时工单进入运营预警与客服复盘")),
                card("闭环关系", "工单 -> 话术 -> 预警",
                        List.of("客服处理工单时可调用已发布话术",
                                "超时风险同步到运营预警扫描",
                                "绩效看板不允许手工造数，只读取工单事实"))
        ));
        return page;
    }

    private List<Map<String, Object>> supportPerformanceRows() {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
                select coalesce(nullif(assignee, ''), '待分配') as agent,
                       count(*) as handled,
                       avg(case when first_response_at is not null then timestampdiff(MINUTE, created_at, first_response_at) else null end) as avg_first_minutes,
                       sum(case when status in ('已解决', '已关闭') then 1 else 0 end) as resolved_count,
                       sum(case when due_at < CURRENT_TIMESTAMP and status not in ('已解决', '已关闭') then 1 else 0 end) as timeout_count
                  from customer_tickets
                 where updated_at >= ?
                 group by coalesce(nullif(assignee, ''), '待分配')
                 order by handled desc, agent asc
                """, LocalDateTime.now().minusDays(30));
        return rows.stream().map(row -> {
            long total = number(row, "handled").longValue();
            long resolved = number(row, "resolved_count").longValue();
            long timeout = number(row, "timeout_count").longValue();
            BigDecimal resolution = ratio(resolved, total);
            long avgMinutes = number(row, "avg_first_minutes").longValue();
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("id", "SUPPORT-" + text(row, "agent"));
            next.put("agent", text(row, "agent"));
            next.put("handled", String.valueOf(total));
            next.put("firstResponse", avgMinutes <= 0 ? "未首响" : (avgMinutes < 60 ? avgMinutes + " 分钟" : BigDecimal.valueOf(avgMinutes).divide(BigDecimal.valueOf(60), 1, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() + " 小时"));
            next.put("resolution", percent(resolution));
            next.put("timeoutCount", String.valueOf(timeout));
            next.put("risk", timeout >= 3 ? "高" : timeout > 0 ? "中" : "低");
            return next;
        }).toList();
    }

    private Map<String, Object> buildScriptLibraryPageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        List<Map<String, Object>> rows = scriptRows();
        long total = rows.size();
        long published = rows.stream().filter(row -> "已发布".equals(row.get("status"))).count();
        long pending = rows.stream().filter(row -> "待审核".equals(row.get("status")) || "草稿".equals(row.get("status"))).count();
        long scenes = rows.stream().map(row -> text(row, "scene")).filter(StringUtils::hasText).distinct().count();
        page.put("title", "话术库管理");
        page.put("desc", "面向 AI 短剧 SaaS 的客服标准回复、升级规则与工单处理知识库");
        page.put("primary", "新增话术");
        page.put("allowCreate", true);
        page.put("allowEdit", true);
        page.put("allowDelete", true);
        page.put("allowProcess", true);
        page.put("workflow", List.of("创建草稿", "质检审核", "发布启用", "工单调用", "效果复盘"));
        page.put("metrics", List.of(
                metric("话术总数", String.valueOf(total), "来自 customer_scripts"),
                metric("已发布", String.valueOf(published), "可在工单中调用"),
                metric("待审核", String.valueOf(pending), "草稿与质检队列"),
                metric("命中场景", String.valueOf(scenes), "客服高频问题")
        ));
        page.put("filters", List.of("分类", "适用场景", "启用状态", "关键词"));
        page.put("columns", List.of(
                column("title", "话术标题", 220, false),
                column("category", "分类", 130, true),
                column("scene", "适用场景", 180, false),
                column("targetRole", "适用对象", 120, true),
                column("triggerKeywords", "触发关键词", 200, false),
                column("status", "状态", 100, true),
                column("updatedAt", "更新时间", 150, false)
        ));
        page.put("rows", rows);
        page.put("cards", List.of(
                card("真实数据源", "customer_scripts + customer_tickets",
                        List.of("话术正文、触发关键词和升级规则读取 customer_scripts",
                                "适用场景与工单分类保持一致，可被工单工作台调用",
                                "状态为已发布的话术才进入客服处理可选范围")),
                card("管理规则", "草稿 -> 待审核 -> 已发布 -> 已下线",
                        List.of("新增话术先写入数据库",
                                "处理按钮会发布话术并更新生效时间",
                                "删除操作为下线并保留维护记录")),
                card("覆盖场景", String.valueOf(scenes) + " 类高频场景",
                        List.of("入驻审核、计费结算、生成任务、订单发票、账号组织、风控合规",
                                "每条话术必须包含标准回复和转交规则",
                                "后续可按工单命中次数做效果复盘"))
        ));
        return page;
    }

    private List<Map<String, Object>> scriptRows() {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
                select id, title, category, scene, target_role, trigger_keywords, reply_template,
                       handoff_rule, tags, updated_by, status, created_at, updated_at
                  from customer_scripts
                 where deleted_at is null
                 order by
                       case status
                         when '待审核' then 1
                         when '草稿' then 2
                         when '已发布' then 3
                         else 4
                       end,
                       updated_at desc
                """);
        return rows.stream().map(row -> {
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("id", text(row, "id"));
            next.put("title", text(row, "title"));
            next.put("category", text(row, "category"));
            next.put("scene", text(row, "scene"));
            next.put("targetRole", text(row, "target_role"));
            next.put("triggerKeywords", text(row, "trigger_keywords"));
            next.put("replyTemplate", text(row, "reply_template"));
            next.put("handoffRule", text(row, "handoff_rule"));
            next.put("tags", text(row, "tags"));
            next.put("updatedBy", text(row, "updated_by"));
            next.put("status", text(row, "status"));
            next.put("updatedAt", formatDateTime(time(row, "updated_at")));
            return next;
        }).toList();
    }

    private Long rowId(List<Map<String, Object>> rows, int rowIndex) {
        if (rowIndex < 0 || rowIndex >= rows.size()) {
            throw new BizException(ErrorCode.NOT_FOUND.getCode(), "记录不存在");
        }
        return number(rows.get(rowIndex), "id").longValue();
    }

    private void addScript(Map<String, Object> row) {
        jdbcTemplate.update("""
                insert into customer_scripts
                (id, title, category, scene, target_role, trigger_keywords, reply_template, handoff_rule, tags, updated_by, status, created_at, updated_at)
                values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """,
                System.currentTimeMillis(),
                requiredText(row, "title", "话术标题不能为空"),
                requestText(row, "category"),
                requestText(row, "scene"),
                requestText(row, "targetRole"),
                requestText(row, "triggerKeywords"),
                requestText(row, "replyTemplate"),
                requestText(row, "handoffRule"),
                requestText(row, "tags"),
                StringUtils.hasText(requestText(row, "updatedBy")) ? requestText(row, "updatedBy") : "运营人员",
                StringUtils.hasText(requestText(row, "status")) ? requestText(row, "status") : "待审核");
    }

    private void updateScript(Long id, Map<String, Object> row) {
        jdbcTemplate.update("""
                update customer_scripts
                   set title = ?, category = ?, scene = ?, target_role = ?, trigger_keywords = ?,
                       reply_template = ?, handoff_rule = ?, tags = ?, updated_by = ?, status = ?, updated_at = CURRENT_TIMESTAMP
                 where id = ?
                """,
                requiredText(row, "title", "话术标题不能为空"),
                requestText(row, "category"),
                requestText(row, "scene"),
                requestText(row, "targetRole"),
                requestText(row, "triggerKeywords"),
                requestText(row, "replyTemplate"),
                requestText(row, "handoffRule"),
                requestText(row, "tags"),
                StringUtils.hasText(requestText(row, "updatedBy")) ? requestText(row, "updatedBy") : "运营人员",
                StringUtils.hasText(requestText(row, "status")) ? requestText(row, "status") : "待审核",
                id);
    }

    private Map<String, Object> buildManualsPageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        List<Map<String, Object>> rows = contentArticleRows("manuals");
        long total = rows.size();
        long published = rows.stream().filter(row -> "已发布".equals(row.get("status"))).count();
        long pending = rows.stream().filter(row -> !"已发布".equals(row.get("status"))).count();
        long weekReads = rows.stream().mapToLong(row -> number(row, "readCount").longValue()).sum();
        long enterprise = rows.stream().filter(row -> text(row, "scope").contains("企业")).count();
        long ops = rows.stream().filter(row -> text(row, "scope").contains("运营")).count();
        page.put("title", "使用手册管理");
        page.put("desc", "企业侧创作手册与运营侧处理手册统一维护、发布和复盘");
        page.put("primary", "新建文档");
        page.put("allowCreate", true);
        page.put("allowEdit", true);
        page.put("allowDelete", true);
        page.put("allowProcess", true);
        page.put("workflow", List.of("编辑文档", "版本校对", "发布上线", "阅读分析"));
        page.put("metrics", List.of(
                metric("文档总数", String.valueOf(total), "企业侧 " + enterprise + " 篇 / 运营侧 " + ops + " 篇"),
                metric("已发布", String.valueOf(published), "可在帮助中心查看"),
                metric("待发布", String.valueOf(pending), "运营流程补充中"),
                metric("本周阅读", String.valueOf(weekReads), "来自 content_articles")
        ));
        page.put("filters", List.of("目录", "发布状态", "适用端", "关键词"));
        page.put("columns", List.of(
                column("title", "文档标题", 240, false),
                column("category", "目录", 130, true),
                column("scope", "适用端", 110, true),
                column("version", "版本", 100, false),
                column("owner", "负责人", 110, true),
                column("readCount", "阅读量", 100, false),
                column("updatedAt", "更新时间", 150, false),
                column("status", "状态", 100, true)
        ));
        page.put("rows", rows);
        page.put("cards", List.of(
                card("真实数据源", "content_articles",
                        List.of("使用手册标题、目录、正文、版本和发布状态均读取内容表",
                                "企业侧/运营侧通过适用端字段区分",
                                "前端预览模板由 front_template 字段驱动")),
                card("发布管理", "版本 + 状态 + 适用端",
                        List.of("待发布文档经校对后再上线",
                                "发布后进入帮助中心可见范围",
                                "删除操作为下线并保留内容历史")),
                card("内容范围", "创作流程 + 运营流程",
                        List.of("企业端覆盖项目、剧本、素材、计费和团队协作",
                                "运营端覆盖入驻审核、订单发票、预警和工单联动",
                                "文档正文支持前端预览和模板化展示"))
        ));
        return page;
    }

    private Map<String, Object> buildAnnouncementsPageData() {
        Map<String, Object> page = new LinkedHashMap<>();
        List<Map<String, Object>> rows = contentArticleRows("announcements");
        long published = rows.stream().filter(row -> "已发布".equals(row.get("status"))).count();
        long scheduled = rows.stream().filter(row -> "定时中".equals(row.get("status")) || "待发布".equals(row.get("status"))).count();
        long forced = rows.stream().filter(row -> text(row, "readMode").contains("强制")).count();
        page.put("title", "政策与新闻");
        page.put("desc", "公告、政策、计费变更与强制阅读配置");
        page.put("primary", "发布公告");
        page.put("allowCreate", true);
        page.put("allowEdit", true);
        page.put("allowDelete", true);
        page.put("allowProcess", true);
        page.put("workflow", List.of("撰写公告", "设置策略", "发布通知", "阅读确认"));
        page.put("metrics", List.of(
                metric("已发布", String.valueOf(published), "来自 content_articles"),
                metric("定时发布", String.valueOf(scheduled), "按发布时间进入发布队列"),
                metric("强制阅读", String.valueOf(forced), "需企业确认")
        ));
        page.put("filters", List.of("公告类型", "发布状态", "是否强制", "发布时间"));
        page.put("columns", List.of(
                column("title", "标题", 260, false),
                column("type", "类型", 110, true),
                column("publishAt", "发布时间", 150, false),
                column("readMode", "阅读要求", 120, true),
                column("frontAudience", "可见对象", 120, true),
                column("readCount", "阅读量", 100, false),
                column("status", "状态", 100, true)
        ));
        page.put("rows", rows);
        page.put("cards", List.of(
                card("真实数据源", "content_articles",
                        List.of("政策、新闻、计费变更与公告统一读取内容表",
                                "发布对象、阅读要求、前端模板均为数据库字段",
                                "强制阅读用于企业登录后的确认弹窗")),
                card("发布策略", "立即 / 定时 / 强制确认",
                        List.of("发布时间晚于当前时间时显示定时中",
                                "重大政策可要求登录确认",
                                "发布动作进入内容记录并保留更新时间")),
                card("前端展示", "公告卡片 + 政策弹窗",
                        List.of("新闻资讯使用卡片模板",
                                "强制确认使用弹窗模板",
                                "普通公告进入通知列表和帮助中心归档"))
        ));
        return page;
    }

    private List<Map<String, Object>> contentArticleRows(String pageKey) {
        String contentType = "manuals".equals(pageKey) ? "manual" : "announcement";
        List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
                select id, content_type, title, category, scope, version, owner, article_type, publish_at,
                       read_mode, status, summary, content, front_template, front_audience, publish_targets,
                       read_count, updated_at
                  from content_articles
                 where deleted_at is null
                   and content_type = ?
                 order by
                       case status
                         when '待发布' then 1
                         when '草稿' then 2
                         when '定时中' then 3
                         when '已发布' then 4
                         else 5
                       end,
                       updated_at desc
                """, contentType);
        return rows.stream().map(row -> {
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("id", text(row, "id"));
            next.put("title", text(row, "title"));
            next.put("category", text(row, "category"));
            next.put("scope", text(row, "scope"));
            next.put("version", text(row, "version"));
            next.put("owner", text(row, "owner"));
            next.put("type", text(row, "article_type"));
            next.put("publishAt", formatDateTime(time(row, "publish_at")));
            next.put("readMode", text(row, "read_mode"));
            next.put("frontTemplate", text(row, "front_template"));
            next.put("frontAudience", text(row, "front_audience"));
            next.put("publishTargets", text(row, "publish_targets"));
            next.put("summary", text(row, "summary"));
            next.put("content", text(row, "content"));
            next.put("readCount", String.valueOf(number(row, "read_count").longValue()));
            next.put("updatedAt", formatDateTime(time(row, "updated_at")));
            next.put("status", text(row, "status"));
            return next;
        }).toList();
    }

    private void addContentArticle(String pageKey, Map<String, Object> row) {
        String contentType = "manuals".equals(pageKey) ? "manual" : "announcement";
        jdbcTemplate.update("""
                insert into content_articles
                (id, content_type, title, category, scope, version, owner, article_type, publish_at, read_mode, status,
                 summary, content, front_template, front_audience, publish_targets, read_count, created_at, updated_at)
                values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                """,
                System.currentTimeMillis(),
                contentType,
                requiredText(row, "title", "标题不能为空"),
                requestText(row, "category"),
                requestText(row, "scope"),
                StringUtils.hasText(requestText(row, "version")) ? requestText(row, "version") : "v1.0",
                StringUtils.hasText(requestText(row, "owner")) ? requestText(row, "owner") : "内容运营",
                requestText(row, "type"),
                parseDateTime(requestText(row, "publishAt")),
                StringUtils.hasText(requestText(row, "readMode")) ? requestText(row, "readMode") : "普通阅读",
                StringUtils.hasText(requestText(row, "status")) ? requestText(row, "status") : ("manuals".equals(pageKey) ? "待发布" : "待发布"),
                requestText(row, "summary"),
                requestText(row, "content"),
                requestText(row, "frontTemplate"),
                requestText(row, "frontAudience"),
                requestText(row, "publishTargets"));
    }

    private void updateContentArticle(Long id, String pageKey, Map<String, Object> row) {
        jdbcTemplate.update("""
                update content_articles
                   set title = ?, category = ?, scope = ?, version = ?, owner = ?, article_type = ?,
                       publish_at = ?, read_mode = ?, status = ?, summary = ?, content = ?,
                       front_template = ?, front_audience = ?, publish_targets = ?, updated_at = CURRENT_TIMESTAMP
                 where id = ?
                """,
                requiredText(row, "title", "标题不能为空"),
                requestText(row, "category"),
                requestText(row, "scope"),
                StringUtils.hasText(requestText(row, "version")) ? requestText(row, "version") : "v1.0",
                StringUtils.hasText(requestText(row, "owner")) ? requestText(row, "owner") : "内容运营",
                requestText(row, "type"),
                parseDateTime(requestText(row, "publishAt")),
                StringUtils.hasText(requestText(row, "readMode")) ? requestText(row, "readMode") : "普通阅读",
                StringUtils.hasText(requestText(row, "status")) ? requestText(row, "status") : ("manuals".equals(pageKey) ? "待发布" : "待发布"),
                requestText(row, "summary"),
                requestText(row, "content"),
                requestText(row, "frontTemplate"),
                requestText(row, "frontAudience"),
                requestText(row, "publishTargets"),
                id);
    }

    private Map<String, Object> warningRow(
            String id,
            String company,
            String type,
            String level,
            String reason,
            String triggerValue,
            String threshold,
            String owner,
            String channels,
            String nextAction,
            String status) {
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("id", id);
        row.put("company", StringUtils.hasText(company) ? company : "未知企业");
        row.put("type", type);
        row.put("level", level);
        row.put("reason", reason);
        row.put("triggerValue", triggerValue);
        row.put("threshold", threshold);
        row.put("owner", owner);
        row.put("channels", channels);
        row.put("nextAction", nextAction);
        row.put("status", status);
        return row;
    }

    private List<Map<String, Object>> reportMetrics() {
        long totalTenants = count("select count(*) from tenants where deleted_at is null");
        long paidTenants = count("""
                select count(distinct tenant_id)
                  from billing_records
                 where credits > 0
                   and (operation = 'recharge' or modality = 'RECHARGE')
                """);
        long repeatTenants = count("""
                select count(*)
                  from (
                        select tenant_id
                          from billing_records
                         where credits > 0
                           and (operation = 'recharge' or modality = 'RECHARGE')
                         group by tenant_id
                        having count(*) >= 2
                       ) t
                """);
        long rechargeCount = count("""
                select count(*)
                  from billing_records
                 where credits > 0
                   and (operation = 'recharge' or modality = 'RECHARGE')
                """);
        BigDecimal rechargeCredits = sum("""
                select coalesce(sum(credits), 0)
                  from billing_records
                 where credits > 0
                   and (operation = 'recharge' or modality = 'RECHARGE')
                """);
        BigDecimal consumedCredits = sum("""
                select coalesce(sum(credits), 0)
                  from billing_records
                 where credits > 0
                   and coalesce(success, true) = true
                   and operation <> 'recharge'
                   and (modality is null or modality <> 'RECHARGE')
                """);

        BigDecimal paidRate = ratio(paidTenants, totalTenants);
        BigDecimal repeatRate = ratio(repeatTenants, paidTenants);
        BigDecimal averageRecharge = rechargeCount == 0
                ? BigDecimal.ZERO
                : rechargeCredits.divide(BigDecimal.valueOf(rechargeCount), 0, RoundingMode.HALF_UP);

        return List.of(
                metric("付费转化率", percent(paidRate), paidTenants + " / " + totalTenants + " 个有效企业"),
                metric("复购率", percent(repeatRate), repeatTenants + " / " + paidTenants + " 个付费企业"),
                metric("平均单笔充值", formatCredits(averageRecharge), rechargeCount + " 笔真实充值流水"),
                metric("积分消耗", formatCredits(consumedCredits), "成功模型调用合计")
        );
    }

    private List<Map<String, Object>> reportRows() {
        List<Map<String, Object>> definitions = jdbcTemplate.queryForList("""
                select code, name, scope, data_source, owner, status, updated_at
                  from admin_report_definitions
                 order by sort_order asc, code asc
                """);
        String sourceUpdatedAt = formatDateTime(latestBusinessUpdatedAt());
        return definitions.stream().map(row -> {
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("name", value(row, "name"));
            next.put("scope", businessText(value(row, "scope")));
            next.put("dataSource", businessText(value(row, "data_source")));
            next.put("updatedAt", sourceUpdatedAt);
            next.put("owner", value(row, "owner"));
            next.put("status", value(row, "status"));
            return next;
        }).toList();
    }

    private List<Map<String, Object>> reportRules() {
        return getReportRules().stream().filter(row -> number(row, "status").intValue() == 1).map(row -> {
            Map<String, Object> next = new LinkedHashMap<>();
            next.put("name", value(row, "name"));
            next.put("scope", value(row, "scope"));
            next.put("formula", value(row, "formula"));
            next.put("owner", value(row, "owner"));
            next.put("editable", value(row, "editable"));
            next.put("notes", value(row, "notes"));
            return next;
        }).toList();
    }

    private List<Map<String, Object>> reportCards() {
        long tenantCount = count("select count(*) from tenants where deleted_at is null");
        long userCount = count("select count(*) from users where deleted_at is null");
        long projectCount = count("select count(*) from projects where deleted_at is null");
        long billingCount = count("select count(*) from billing_records");
        long modelCount = count("""
                select count(*)
                  from (
                        select model_provider, model_id
                          from billing_records
                         where model_id is not null
                         group by model_provider, model_id
                       ) t
                """);
        long ruleCount = count("select count(*) from admin_report_rules where status = 1");
        long activeTenantCount = activeTenants();
        long dormantTenantCount = Math.max(tenantCount - activeTenantCount, 0);
        return List.of(
                card("真实数据源", "5 类业务表",
                        List.of("租户 " + tenantCount + " 个、员工 " + userCount + " 个、项目 " + projectCount + " 个",
                                "资源流水 " + billingCount + " 笔、实际模型 " + modelCount + " 个",
                                "报表目录来自 admin_report_definitions，不读取静态页面 JSON")),
                card("算法口径", ruleCount + " 条规则",
                        List.of("规则说明来自 admin_report_rules，可在数据库维护",
                                "充值、复购、消耗和活跃分层均按当前业务表计算",
                                "缺少订单实付金额表时，客单价暂以充值积分流水作为替代口径")),
                card("活跃分层", activeTenantCount + " 活跃 / " + dormantTenantCount + " 沉睡",
                        List.of("近 7 天登录、近 14 天项目更新或资源流水均视为活跃",
                                "沉睡租户可进入预警与客服回访流程",
                                "所有统计排除 deleted_at 非空的租户和用户"))
        );
    }

    private Map<String, Object> column(String prop, String label, int minWidth, boolean tag) {
        Map<String, Object> column = new LinkedHashMap<>();
        column.put("prop", prop);
        column.put("label", label);
        column.put("minWidth", minWidth);
        if (tag) {
            column.put("tag", true);
        }
        return column;
    }

    private Map<String, Object> metric(String label, String value, String hint) {
        Map<String, Object> metric = new LinkedHashMap<>();
        metric.put("label", businessText(label));
        metric.put("value", businessText(value));
        metric.put("hint", businessText(hint));
        return metric;
    }

    private Map<String, Object> card(String title, String value, List<String> items) {
        Map<String, Object> card = new LinkedHashMap<>();
        card.put("title", businessText(title));
        card.put("value", businessText(value));
        card.put("items", items.stream().map(this::businessText).toList());
        return card;
    }

    private Map<String, Object> rule(String name, String scope, String formula, String owner, String editable, String notes) {
        Map<String, Object> rule = new LinkedHashMap<>();
        rule.put("name", businessText(name));
        rule.put("scope", businessText(scope));
        rule.put("formula", businessText(formula));
        rule.put("owner", businessText(owner));
        rule.put("editable", businessText(editable));
        rule.put("notes", businessText(notes));
        return rule;
    }

    private long activeTenants() {
        return count("""
                select count(distinct t.id)
                  from tenants t
                 where t.deleted_at is null
                   and (
                        exists (select 1 from users u where u.tenant_id = t.id and u.deleted_at is null and u.last_login_at >= ?)
                        or exists (select 1 from projects p where p.tenant_id = t.id and p.deleted_at is null and p.updated_at >= ?)
                        or exists (select 1 from billing_records b where b.tenant_id = t.id and b.created_at >= ?)
                   )
                """, LocalDateTime.now().minusDays(7), LocalDateTime.now().minusDays(14), LocalDateTime.now().minusDays(14));
    }

    private LocalDateTime latestBusinessUpdatedAt() {
        List<LocalDateTime> times = new ArrayList<>();
        addIfNotNull(times, latest("select max(updated_at) from tenants"));
        addIfNotNull(times, latest("select max(updated_at) from users"));
        addIfNotNull(times, latest("select max(updated_at) from projects"));
        addIfNotNull(times, latest("select max(created_at) from billing_records"));
        addIfNotNull(times, latest("select max(updated_at) from tenant_quotas"));
        addIfNotNull(times, latest("select max(updated_at) from admin_report_rules"));
        return times.stream().max(LocalDateTime::compareTo).orElse(LocalDateTime.now());
    }

    private void addIfNotNull(List<LocalDateTime> times, LocalDateTime time) {
        if (time != null) {
            times.add(time);
        }
    }

    private long count(String sql, Object... args) {
        Number value = jdbcTemplate.queryForObject(sql, Number.class, args);
        return value == null ? 0 : value.longValue();
    }

    private BigDecimal sum(String sql) {
        BigDecimal value = jdbcTemplate.queryForObject(sql, BigDecimal.class);
        return value == null ? BigDecimal.ZERO : value;
    }

    private BigDecimal sum(String sql, Object... args) {
        BigDecimal value = jdbcTemplate.queryForObject(sql, BigDecimal.class, args);
        return value == null ? BigDecimal.ZERO : value;
    }

    private LocalDateTime latest(String sql) {
        return jdbcTemplate.query(sql, rs -> rs.next() ? rs.getObject(1, LocalDateTime.class) : null);
    }

    private Long lookupLong(String sql, Object... args) {
        return jdbcTemplate.query(sql, rs -> rs.next() ? rs.getLong(1) : null, args);
    }

    private long resolveTenantId(Map<String, Object> row) {
        String explicit = requestText(row, "tenantId");
        if (StringUtils.hasText(explicit)) {
            try {
                return Long.parseLong(explicit);
            } catch (NumberFormatException ignored) {
                // continue with business fields
            }
        }
        String company = requiredText(row, "company", "请选择企业");
        Long id = lookupLong("""
                select id
                  from tenants
                 where deleted_at is null
                   and (name = ? or slug = ?)
                 limit 1
                """, company, company);
        if (id == null) {
            throw new BizException(ErrorCode.NOT_FOUND.getCode(), "企业不存在，请先在企业列表中创建");
        }
        return id;
    }

    private long resolveUserId(long tenantId, Map<String, Object> row) {
        String explicit = requestText(row, "userId");
        if (StringUtils.hasText(explicit)) {
            try {
                return Long.parseLong(explicit);
            } catch (NumberFormatException ignored) {
                // continue with account fields
            }
        }
        String member = firstToken(requiredText(row, "member", "请选择成员账号"));
        String account = requestText(row, "account");
        Long id = lookupLong("""
                select id
                  from users
                 where tenant_id = ?
                   and deleted_at is null
                   and (username = ? or email = ? or phone = ? or username = ? or email = ? or phone = ?)
                 limit 1
                """, tenantId, member, member, member, account, account, account);
        if (id == null) {
            throw new BizException(ErrorCode.NOT_FOUND.getCode(), "成员账号不存在，请先在团队管理中创建");
        }
        return id;
    }

    private String resolvePolicyCode(Map<String, Object> row) {
        String explicit = requestText(row, "policyCode");
        String feature = StringUtils.hasText(explicit) ? explicit : requiredText(row, "feature", "请选择功能权限");
        String code = jdbcTemplate.query("""
                select code
                  from permission_policies
                 where status = 1
                   and (code = ? or name = ?)
                 limit 1
                """, rs -> rs.next() ? rs.getString(1) : null, feature, feature);
        if (!StringUtils.hasText(code)) {
            throw new BizException(ErrorCode.NOT_FOUND.getCode(), "功能权限不存在，请从固定权限项中选择");
        }
        return code;
    }

    private String memberRole(long tenantId, long userId) {
        String role = jdbcTemplate.query("""
                select role
                  from tenant_members
                 where tenant_id = ? and user_id = ?
                 limit 1
                """, rs -> rs.next() ? rs.getString(1) : null, tenantId, userId);
        return StringUtils.hasText(role) ? role : "member";
    }

    private String dataScopeForRole(String role) {
        if ("owner".equalsIgnoreCase(role)) return "企业全部数据";
        if ("admin".equalsIgnoreCase(role)) return "企业运营数据";
        return "本人参与项目";
    }

    private String roleLabel(String role) {
        if ("owner".equalsIgnoreCase(role)) return "所有者";
        if ("admin".equalsIgnoreCase(role)) return "管理员";
        if ("member".equalsIgnoreCase(role)) return "成员";
        return StringUtils.hasText(role) ? role : "未配置";
    }

    private String accountText(Map<String, Object> row) {
        List<String> parts = new ArrayList<>();
        if (StringUtils.hasText(text(row, "email"))) parts.add(text(row, "email"));
        if (StringUtils.hasText(text(row, "phone"))) parts.add(text(row, "phone"));
        return parts.isEmpty() ? "-" : String.join(" / ", parts);
    }

    private String firstToken(String value) {
        if (!StringUtils.hasText(value)) {
            return "";
        }
        String normalized = value.trim();
        for (String separator : List.of(" / ", "/", "、", " ")) {
            int index = normalized.indexOf(separator);
            if (index > 0) {
                return normalized.substring(0, index).trim();
            }
        }
        return normalized;
    }

    private String normalizeProtocol(Map<String, Object> row) {
        String value = StringUtils.hasText(requestText(row, "protocolCode")) ? requestText(row, "protocolCode") : requestText(row, "protocol");
        if (!StringUtils.hasText(value)) {
            return "volcengine";
        }
        if (value.contains("火山")) return "volcengine";
        if (value.toLowerCase().contains("openai")) return "openai";
        if (value.contains("通义") || value.toLowerCase().contains("dashscope")) return "dashscope";
        if (value.toLowerCase().contains("gemini")) return "gemini";
        if (value.toLowerCase().contains("kling")) return "kling";
        return value.trim();
    }

    private String protocolLabel(String protocol) {
        if (!StringUtils.hasText(protocol)) {
            return "未配置";
        }
        return switch (protocol.toLowerCase()) {
            case "volcengine" -> "火山引擎";
            case "openai" -> "OpenAI";
            case "dashscope" -> "通义千问";
            case "gemini" -> "Gemini";
            case "kling" -> "可灵";
            default -> protocol;
        };
    }

    private String normalizeCapability(Map<String, Object> row) {
        String value = StringUtils.hasText(requestText(row, "capabilityCode")) ? requestText(row, "capabilityCode") : requestText(row, "capability");
        if (!StringUtils.hasText(value)) {
            return "text";
        }
        if (value.contains("图") || value.equalsIgnoreCase("IMAGE")) return "image";
        if (value.contains("视频") || value.equalsIgnoreCase("VIDEO")) return "video";
        if (value.contains("音频") || value.equalsIgnoreCase("AUDIO")) return "audio";
        return "text";
    }

    private String capabilityLabel(String capability) {
        if (!StringUtils.hasText(capability)) {
            return "未配置";
        }
        return switch (capability.toLowerCase()) {
            case "image" -> "图片生成";
            case "video" -> "视频生成";
            case "audio" -> "音频生成";
            case "text" -> "文本生成";
            default -> capability;
        };
    }

    private String capabilityFromModality(String modality) {
        if (!StringUtils.hasText(modality)) {
            return "text";
        }
        if (modality.equalsIgnoreCase("IMAGE")) return "image";
        if (modality.equalsIgnoreCase("VIDEO")) return "video";
        if (modality.equalsIgnoreCase("AUDIO")) return "audio";
        return "text";
    }

    private String modelSummary(String modelsJson) {
        if (!StringUtils.hasText(modelsJson) || "[]".equals(modelsJson.trim())) {
            return "未配置模型";
        }
        try {
            List<Map<String, Object>> models = objectMapper.readValue(modelsJson, new TypeReference<>() {
            });
            List<String> names = models.stream()
                    .map(model -> StringUtils.hasText(text(model, "name")) ? text(model, "name") : text(model, "id"))
                    .filter(StringUtils::hasText)
                    .limit(3)
                    .toList();
            if (!names.isEmpty()) {
                return String.join("、", names) + (models.size() > names.size() ? " 等 " + models.size() + " 个" : "");
            }
        } catch (Exception ignored) {
            // fall back to a compact count
        }
        int count = Math.max(1, modelsJson.split("\"id\"").length - 1);
        return count + " 个模型";
    }

    private String defaultModelText(long providerId) {
        String model = jdbcTemplate.query("""
                select model_id
                  from tenant_model_defaults
                 where provider_id = ?
                 limit 1
                """, rs -> rs.next() ? rs.getString(1) : null, providerId);
        return StringUtils.hasText(model) ? model : "未设置";
    }

    private String modelJson(String modelId, String modelName) {
        try {
            Map<String, Object> model = new LinkedHashMap<>();
            model.put("id", modelId);
            model.put("name", modelName);
            model.put("checked", true);
            return objectMapper.writeValueAsString(List.of(model));
        } catch (Exception e) {
            return "[]";
        }
    }

    private BigDecimal ratio(long numerator, long denominator) {
        if (denominator <= 0) {
            return BigDecimal.ZERO;
        }
        return BigDecimal.valueOf(numerator)
                .multiply(BigDecimal.valueOf(100))
                .divide(BigDecimal.valueOf(denominator), 1, RoundingMode.HALF_UP);
    }

    private String percent(BigDecimal value) {
        return value.stripTrailingZeros().toPlainString() + "%";
    }

    private String formatCredits(BigDecimal value) {
        if (value == null) {
            return "0";
        }
        BigDecimal abs = value.abs();
        if (abs.compareTo(BigDecimal.valueOf(10000)) >= 0) {
            return value.divide(BigDecimal.valueOf(10000), 1, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString() + "万";
        }
        return value.setScale(0, RoundingMode.HALF_UP).toPlainString();
    }

    private String formatDateTime(LocalDateTime time) {
        return time == null ? "-" : DATE_TIME_FORMATTER.format(time);
    }

    private String durationText(LocalDateTime start, LocalDateTime end) {
        if (start == null || end == null) {
            return "未首响";
        }
        long minutes = java.time.Duration.between(start, end).toMinutes();
        if (minutes < 60) {
            return minutes + " 分钟";
        }
        BigDecimal hours = BigDecimal.valueOf(minutes)
                .divide(BigDecimal.valueOf(60), 1, RoundingMode.HALF_UP)
                .stripTrailingZeros();
        return hours.toPlainString() + " 小时";
    }

    private LocalDateTime parseDateTime(String value) {
        if (!StringUtils.hasText(value) || "-".equals(value)) {
            return LocalDateTime.now();
        }
        List<DateTimeFormatter> formatters = List.of(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
                DATE_TIME_FORMATTER
        );
        for (DateTimeFormatter formatter : formatters) {
            try {
                return LocalDateTime.parse(value, formatter);
            } catch (Exception ignored) {
                // try next formatter
            }
        }
        try {
            return LocalDateTime.parse(value);
        } catch (Exception ignored) {
            return LocalDateTime.now();
        }
    }

    private Object value(Map<String, Object> row, String key) {
        Object value = row.get(key);
        if (value != null) {
            return value;
        }
        value = row.get(key.toUpperCase());
        if (value != null) {
            return value;
        }
        return row.get(toCamelCase(key));
    }

    private String businessText(Object value) {
        if (value == null) {
            return "";
        }
        String text = String.valueOf(value);
        return text
                .replace("tenant_members", "成员关系")
                .replace("tenant_balances", "企业余额")
                .replace("tenant_quotas", "套餐配额")
                .replace("billing_records", "资源流水")
                .replace("admin_report_definitions", "报表目录")
                .replace("admin_report_rules", "报表规则")
                .replace("ai_tasks", "AI 任务")
                .replace("projects", "短剧项目")
                .replace("tenants", "企业租户")
                .replace("users", "组织员工")
                .replace("plans", "套餐配置")
                .replace("model_id", "模型名称")
                .replace("model_provider", "模型供应商")
                .replace("tenant_id", "企业")
                .replace("deleted_at", "删除时间")
                .replace("updated_at", "更新时间")
                .replace("created_at", "创建时间")
                .replace("operation=recharge", "充值流水")
                .replace("modality=RECHARGE", "充值流水")
                .replace("credits>0", "积分增加")
                .replace("success=true", "成功调用")
                .replace("success = true", "成功调用")
                .replace("operation <> 'recharge'", "非充值流水");
    }

    private String text(Map<String, Object> row, String key) {
        Object value = value(row, key);
        return value == null ? "" : String.valueOf(value);
    }

    private Number number(Map<String, Object> row, String key) {
        Object value = value(row, key);
        if (value instanceof Number number) {
            return number;
        }
        if (value == null || !StringUtils.hasText(String.valueOf(value))) {
            return 0;
        }
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (NumberFormatException e) {
            return 0;
        }
    }

    private BigDecimal decimal(Map<String, Object> row, String key) {
        Object value = value(row, key);
        if (value instanceof BigDecimal decimal) {
            return decimal;
        }
        if (value instanceof Number number) {
            return BigDecimal.valueOf(number.doubleValue());
        }
        if (value == null || !StringUtils.hasText(String.valueOf(value))) {
            return BigDecimal.ZERO;
        }
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (NumberFormatException e) {
            return BigDecimal.ZERO;
        }
    }

    private LocalDateTime time(Map<String, Object> row, String key) {
        Object value = value(row, key);
        if (value instanceof LocalDateTime localDateTime) {
            return localDateTime;
        }
        if (value instanceof java.sql.Timestamp timestamp) {
            return timestamp.toLocalDateTime();
        }
        return null;
    }

    private int intSetting(Map<String, Object> settings, String key, int fallback) {
        Object value = settings.get(key);
        if (value instanceof Number number) {
            return number.intValue();
        }
        if (value != null && StringUtils.hasText(String.valueOf(value))) {
            try {
                return Integer.parseInt(String.valueOf(value));
            } catch (NumberFormatException ignored) {
                return fallback;
            }
        }
        return fallback;
    }

    private String requestText(Map<String, Object> payload, String key) {
        Object value = payload.get(key);
        if (value == null) {
            return "";
        }
        return String.valueOf(value).trim();
    }

    private String requiredText(Map<String, Object> payload, String key, String message) {
        String value = requestText(payload, key);
        if (!StringUtils.hasText(value)) {
            throw new BizException(ErrorCode.INVALID_PARAM.getCode(), message);
        }
        return value;
    }

    private int requestInt(Map<String, Object> payload, String key, int fallback) {
        Object value = payload.get(key);
        if (value instanceof Number number) {
            return number.intValue();
        }
        if (value != null && StringUtils.hasText(String.valueOf(value))) {
            try {
                return Integer.parseInt(String.valueOf(value));
            } catch (NumberFormatException ignored) {
                return fallback;
            }
        }
        return fallback;
    }

    private String planLabel(String plan) {
        if (!StringUtils.hasText(plan)) {
            return "未配置";
        }
        return switch (plan.trim().toLowerCase()) {
            case "free" -> "免费版";
            case "pro" -> "专业版";
            case "team" -> "团队版";
            case "enterprise" -> "企业版";
            default -> plan;
        };
    }

    private String toCamelCase(String key) {
        StringBuilder builder = new StringBuilder();
        boolean upperNext = false;
        for (char c : key.toCharArray()) {
            if (c == '_') {
                upperNext = true;
            } else if (upperNext) {
                builder.append(Character.toUpperCase(c));
                upperNext = false;
            } else {
                builder.append(c);
            }
        }
        return builder.toString();
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> rowsOf(Map<String, Object> page) {
        Object rows = page.get("rows");
        if (rows instanceof List<?> list) {
            List<Map<String, Object>> result = new ArrayList<>();
            for (Object item : list) {
                if (item instanceof Map<?, ?> map) {
                    result.add(new LinkedHashMap<>((Map<String, Object>) map));
                }
            }
            return result;
        }
        return new ArrayList<>();
    }

    private void writePage(String pageKey, Map<String, Object> page) {
        try {
            String payload = objectMapper.writeValueAsString(page);
            int updated = jdbcTemplate.update(
                    "update admin_page_data set payload_json = ?, updated_at = CURRENT_TIMESTAMP where page_key = ?",
                    payload,
                    pageKey);
            if (updated == 0) {
                throw new BizException(ErrorCode.NOT_FOUND.getCode(), "页面数据未配置");
            }
        } catch (BizException e) {
            throw e;
        } catch (Exception e) {
            throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "页面数据保存失败");
        }
    }
}
