智能安全运营平台从0到1：构建企业级 AI 驱动的安全运营体系 从告警风暴到智能研判，从人工处置到自动化闭环 —— 我们如何用大语言模型、LangGraph、RAG 和多设备联动，构建一个 AI-Native 安全运营平台。 一、写在前面 1.1 安全运营的困境 如果你在一家中大型企业的安全团队工作，大概率会面临以下困境： 告警风暴：每天数千条安全告警涌入，WAF、EDR、DLP、防火墙各自为战，安全分析师疲于"看告警、判误报"的机械劳动； 信息孤岛：攻击者的一个完整攻击链（扫描 -> 漏洞利用 -> 提权 -> 数据外传）被分散在多个设备日志中，人工难以快速关联； 处置缓慢：确认真实威胁后，需要登录多个控制台手动封禁 IP，操作链路长、易出错，平均耗时 5-10 分钟； 知识断层：安全 SOP、法规解读、历史案例散落在文档、Wiki 和工程师的大脑里，新人上手周期长达 2-3 周； 合规压力：一份合同/产品文档的合规审查，需要资深法务 4-6 小时逐条比对法规，效率极低。 传统 SOAR（安全编排自动化与响应）平台虽然能解决一部分自动化问题，但剧本（Playbook）编写成本高、灵活性差，面对层出不穷的攻击手法显得捉襟见肘。 1.2 我们的答案 2024 年以来，大语言模型（LLM）的能力突飞猛进。我们开始思考一个根本性的问题：能不能让 AI 像一个资深安全分析师一样，理解告警、研判风险、决策处置，甚至直接与安全设备对话？ 二、平台核心亮点总览 在深入技术细节之前，先通过一张全景图了解平台的六大核心能力： ┌──────────────────────────────────────────────────────────────────┐ │ 智能安全运营平台 六大核心能力 │ ├────────────┬──────────────┬──────────────┬─────────────────────────┤ │ ① 多源 │ ② AI智能 │ ③ 自动化 │ ④ 对话式 ⑤ RAG │ │ 告警聚合 │ 威胁研判 │ 处置闭环 │ 安全助手 知识增强 │ │ │ │ │ │ │ WAF/Sys │ 7+专用LLM │ AI决策→ │ 40+工具调用 6种切分 │ │ mon/牧云/ │ Agent自动 │ 多设备执行 │ LangGraph 策略 │ │ 深信服/DLP│ 分析研判 │ →飞书反馈 │ 流式对话 Milvus │ ├────────────┴──────────────┴──────────────┴─────────────────────────┤ │ ⑥ 合规审查 4-Agent 流水线 │ │ 文档理解 → 条款精排 → 逐条审查 → 汇总报告（支持7法域） │ └──────────────────────────────────────────────────────────────────┘ 下面，我们逐一深入剖析每个核心亮点的设计思路、技术实现和关键决策。 三、亮点一：多源告警聚合与智能预处理 3.1 告警归一化 安全运营的第一大痛点是告警碎片化。我们的平台接入了五种核心安全数据源： 数据源 覆盖范围 接入方式 日均告警量 腾讯云 WAF + 飞塔 WAF Web 应用攻击检测（SQL注入、XSS、命令注入等） API 轮询 + Webhook 推送 5000+ Sysmon 终端日志 办公终端异常行为（进程创建、网络连接、注册表修改） Splunk 定时拉取 3000+ 牧云 EDR 服务器主机安全（恶意文件、反弹Shell、提权检测） API 定时同步 1000+ 深信服 STA 高级持续威胁检测（横向移动、C2通信、数据窃取） API 定时同步 500+ DLP 系统 数据泄露防护（敏感文件外传、异常下载行为） API 定时同步 300+ 每种数据源接入时，第一步不是直接丢给 AI，而是经过三层预处理流水线： 原始告警 → [预过滤层] → [去重层] → [上下文增强层] → 标准化告警卡片 预过滤层：基于规则引擎剔除明确的误报，包括内部扫描器 IP、自动化健康检查、已知白名单行为。规则以正则表达式 + IP 白名单的形式配置，可动态更新。 去重层：使用告警指纹（源IP + 目标 + 告警类型 + 时间窗口）计算 MD5，配合 Redis 分布式锁，保证同一告警不会在 5 分钟内被重复处理。 上下文增强层：对于首次出现的告警，自动查询关联数据 —— 该源 IP 的历史告警记录、在 CMDB 中的资产信息、威胁情报查询结果，一并注入告警卡片。 3.2 异步任务调度架构 告警处理天然适合异步解耦。我们基于 Celery 构建了多队列任务调度体系： Celery Beat (定时器，每30秒) │ ├─→ waf 队列 (WAF 告警批量分析) ├─→ sysmon 队列 (Sysmon 告警批量分析) ├─→ muyun 队列 (牧云主机告警分析) ├─→ dlp 队列 (DLP 告警分析) ├─→ sangfor 队列 (深信服 STA 告警分析) └─→ compliance 队列 (合规审查任务) 为什么分 6 个独立队列？ 故障隔离：WAF 告警的 LLM 调用卡顿不会影响 Sysmon 或合规审查任务； 优先级控制：不同队列可以配置不同的 Worker 数量和消费速率； 资源隔离：WAF 和 DLP 告警量大，可以分配更多 Worker 线程。 每个 Worker 线程池 10 并发，总处理能力可达每秒 60+ 条告警。 四、亮点二：AI 智能威胁研判 Agent 体系 4.1 专用 Agent 矩阵 这是平台的核心大脑。我们没有做一个"万能安全 Agent"，而是为每种告警类型设计了专用的 LLM Agent： ┌─────────────────────────────────────────────────────┐ │ AI Agent 矩阵 │ ├──────────────┬──────────────┬───────────────────────┤ │ WafAgent │ SysmonAgent │ MuyunHostAlertAgent │ │ WAF 攻击研判 │ 终端行为分析 │ 主机入侵检测 │ │ 攻击成功性 │ 恶意进程链 │ ATT&CK 阶段映射 │ │ Payload 解码 │ 横向移动识别 │ 入侵深度评估 │ ├──────────────┼──────────────┼───────────────────────┤ │ SangforAgent │ AnalyzerAgent│ SecurityActionAgent │ │ 深信服 STA │ Splunk 结果 │ 统一安全处置决策 │ │ 关联告警分析 │ 二次深度分析 │ Function Calling 执行 │ │ 攻击路径还原 │ 异常模式识别 │ 多设备联动封禁 │ └──────────────┴──────────────┴───────────────────────┘ 4.2 Agent 设计的三要素 每个 Agent 都由三部分组成，缺一不可： ① 系统提示词 —— 人工精心调校的"角色设定" 提示词是整个 Agent 的灵魂。以 WAF 分析 Agent 为例，其 92KB 的提示词包含： 角色定义："你是一位拥有 10 年经验的 Web 安全分析专家..." 分析框架：攻击模式分类表（SQL 注入 / XSS / 命令注入 / 文件包含 / SSRF 等 12 类） 编码混淆手法识别：Base64、URL 编码、Unicode 编码、多层嵌套编码的判定规则 威胁等级定义：Critical（已成功利用）/ High（高可能性）/ Medium（可疑行为）/ Low（探测扫描） 可信度评估规则：Payload 有效性、攻击链完整性、历史行为关联度 输出格式约束：严格的 JSON Schema，包含 title、rank、detail、analyze、advice 五个必填字段 ② 上下文构建器 —— 不只是"告警本身" Agent 不是只看眼前这条告警，而是通过 AlertContext 协议自动聚合多维度上下文： # 伪代码：WAF 告警的上下文构建 context = { # 告警自身 "source_ip": "203.0.113.42", "target_url": "/api/user/login", "payload": "admin' OR '1'='1", "attack_type": "SQL Injection", # 跨源关联上下文（自动查询） "sysmon_events": [ # 同一 IP 在终端上的行为 {"event_id": 1, "process": "cmd.exe", "command": "whoami"}, ], "threat_intel": { # 威胁情报查询结果 "is_malicious": True, "tags": ["scanner", "brute_force"], "first_seen": "2024-01-15" }, "asset_info": { # 目标资产信息 "server_name": "核心用户系统", "business_line": "用户中心", "criticality": "high" }, "historical_alerts": [ # 该 IP 的历史告警 {"time": "-10min", "type": "目录扫描", "count": 47}, {"time": "-5min", "type": "SQL注入", "count": 12}, ] } 这种"上帝视角"的上下文让 Agent 能够： 判断攻击是否是孤立事件还是持续性攻击； 结合威胁情报判断 IP 的信誉度； 根据目标资产重要性调高或调低威胁等级。 ③ LLM 调用层 —— 稳定性和结构化输出保障 大模型的输出天然具有不确定性，而我们在生产环境中要求严格的 JSON 输出。为此设计了三重保障： 约束型提示词：明确的正则和枚举约束，LLM 温度设为 0.1-0.3； 输出修复层：正则提取 JSON 块 → json.loads() 尝试解析 → 失败则启发式修复（补括号、修引号、截取有效 JSON）； Schema 校验：解析后的 JSON 经过 Pydantic 风格的字段校验，不合法则重试（最多 2 次）。 经过这三重保障，Agent 的 JSON 有效率达 98% 以上。 4.3 批量分析策略 如果每条告警单独调一次 LLM，Token 消耗可能达到天文数字。我们的解决方案是时间窗口批量分析： 时间窗口（5分钟） ├── 告警 1: 源IP 203.0.113.42 → SQL注入 → /api/login ├── 告警 2: 源IP 203.0.113.42 → SQL注入 → /api/search ├── 告警 3: 源IP 203.0.113.42 → XSS → /api/comment ├── 告警 4: 源IP 198.51.100.7 → 目录扫描 → /* └── 告警 5: 源IP 198.51.100.7 → 文件包含 → /download → 一次 LLM 调用，输出 5 条告警的结构化分析结果 同一时间窗口、同一数据源的所有待分析告警打包为一个批次（通常 5-20 条），一次 LLM 调用完成整批分析。Token 消耗降低为逐条分析的 1/5 到 1/3。 五、亮点三：自动化处置闭环 —— 从"看了告警"到"解决了威胁" 5.1 处置链路全景 判断出威胁只是第一步，快速处置才是最终目标。我们的处置链路设计： ┌─────────────────────────────────────────────────────────────────┐ │ 自动化处置全链路 │ │ │ │ ① 飞书卡片推送 │ │ ┌─────────────────────────────────┐ │ │ │ ⚠️ Critical: SQL注入攻击 │ │ │ │ 源IP: 203.0.113.42 │ │ │ │ AI研判: 攻击payload绕过了WAF规则 │ │ │ │ [封禁IP] [加白名单] [忽略] [详情] │ ← 用户点击按钮 │ │ └─────────────────────────────────┘ │ │ ↓ │ │ ② WebSocket 回调 → LarkCallbackHandler │ │ - 二次确认弹窗 │ │ - 权限校验（飞书用户 → 平台用户 → RBAC 权限） │ │ ↓ │ │ ③ SecurityActionAgent.Function_Calling() │ │ AI 决策: │ │ - 封禁对象: 203.0.113.42 │ │ - 封禁设备: alicloud_firewall + tencent_waf │ │ - 封禁时长: 24小时（SQL注入常规封禁） │ │ - 理由: "SQL注入攻击payload确认真实，IP历史有恶意行为" │ │ ↓ │ │ ④ ToolRegistry → 调度执行 │ │ ├─ alicloud_firewall_tool.freeze_ip(203.0.113.42, duration=24h)│ │ └─ tencent_waf_tool.ban_ip(203.0.113.42) │ │ ↓ │ │ ⑤ 结果回写 │ │ - 飞书卡片状态更新: "已封禁" │ │ - 审计日志: 操作人/时间/对象/结果/原始告警 │ │ - Redis 可回滚 Token (30天内可一键解封) │ └─────────────────────────────────────────────────────────────────┘ 5.2 SecurityActionAgent —— AI 驱动的处置决策 传统 SOAR 的 Playbook 是硬的 —— 规则写死，场景变化就得改。我们的 SecurityActionAgent 通过 Function Calling 让 AI 自主决策： class SecurityActionAgent: """ 统一安全处置Agent 核心设计理念：与告警类型完全解耦。 通过 AlertContext 协议接收输入，通过 ToolRegistry 发现可用工具。 新增告警类型或新接入安全设备，无需修改此 Agent。 """ def process(self, ctx: AlertContext) -> ActionResult: # 第一步：AI 分析并制定处置计划 plan = self.analyze_and_plan(ctx) # Function Calling # 第二步：执行处置计划 return self.execute_plan(plan) def analyze_and_plan(self, ctx): # 根据 ctx.target（域名/主机名）自动匹配可用工具 tools = tool_registry.to_function_definitions(domain=ctx.target) # 调用 LLM，让 AI 决定：用什么工具、什么参数 tool_calls, text = call_with_tools( model=self.model, system_prompt=自行构建的处置提示词， user_message=ctx.to_user_message(), tools=tools, temperature=0.1, ) 为什么让 AI 决策而不是写死逻辑？ 举一个真实案例：某 WAF 告警显示 203.0.113.42 在进行 SQL 注入。AI 的决策逻辑是这样的： 检查 ctx.analyze（AI 前置研判）：攻击 Payload 确认有效，且目标返回了数据库错误 → 攻击真实； 检查 ctx.historical_alerts：该 IP 在 10 分钟前进行了 47 次目录扫描 → 非孤立事件，是定向攻击； 检查 ctx.threat_intel：该 IP 来自已知的恶意扫描器 IP 段 → 高度可疑； 检查 ctx.asset_info：目标是核心用户系统的登录接口 → 影响面大； 决策：alicloud_firewall 封禁 24 小时 + tencent_waf 黑名单永久封禁，双设备联动。 这种"综合分析 + 弹性决策"的能力，是传统 if-else Playbook 无法实现的。 5.3 工具注册表 —— 安全设备的统一抽象 ToolRegistry 将所有安全设备的操作抽象为统一的 Tool 接口： 工具 设备 操作 目标类型 alicloud_firewall_tool 阿里云防火墙 freeze_ip / unfreeze_ip IP alicloud_ddos_tool 阿里云 DDoS 高防 add_blacklist / remove_blacklist IP tencent_waf_tool 腾讯云 WAF ban_ip / unban_ip IP fortiweb_tool 飞塔 WAF block_ip / unblock_ip IP muyun_host_tool 牧云 EDR isolate_host / restore_host Host 每个工具继承自 BaseActionTool 抽象基类，实现三个核心方法： to_function_definition() —— 生成 OpenAI Function Calling 的 JSON Schema； execute(action_type, params) —— 调用设备 API 执行操作； validate(params) —— 参数合法性校验。 新增一个安全设备只需实现一个工具类并注册，无需修改 SecurityActionAgent 的任何代码。这种"Agent 与工具完全解耦"的设计，让平台的设备扩展成本接近零。 5.4 飞书深度集成 —— 让处置"随手可达" 为什么选飞书而不是自建前端？ 安全运营人员日常已经在飞书中处理消息、协作沟通。将告警通知和处置操作直接嵌入飞书，意味着不用切换工具，边聊天边处置。 飞书卡片交互示意图： ┌──────────────────────────────┐ │ 🔴 Critical：Web应用攻击 │ │ │ │ 攻击IP：203.0.113.42 │ │ 攻击类型：SQL盲注 │ │ 目标URL：/api/user/login │ │ │ │ AI研判： │ │ 该攻击payload经过三层编码绕 │ │ 过WAF规则，请求返回了数据库 │ │ 异常，攻击成功可能性极高。 │ │ │ │ 建议：立即封禁源IP │ │ │ │ [🔒 封禁IP] [🔓 加白名单] │ │ [📋 查看详情] [⏭ 忽略] │ └──────────────────────────────┘ ↓ 点击 [封禁IP] ┌──────────────────────────────┐ │ ⚠️ 确认封禁 │ │ IP: 203.0.113.42 │ │ 时长: [24小时 ▼] │ │ 范围: [阿里云防火墙 ▼] │ │ [确认封禁] [取消] │ └──────────────────────────────┘ ↓ 点击 [确认封禁] ┌──────────────────────────────┐ │ ✅ 处置完成 │ │ IP: 203.0.113.42 已封禁 │ │ 设备: 阿里云防火墙 │ │ 封禁至: 明天 14:30 │ │ 操作人: 张三 │ │ [🔄 撤销封禁] [📋 详情] │ └──────────────────────────────┘ 飞书客户端基于 lark_oapi 实现，通过 WebSocket 长连接监听卡片按钮回调（P2CardActionTrigger），连接断线自动重连（支持指数退避），确保消息不丢失。 六、亮点四：安全运营 AI 对话助手 6.1 基于 LangGraph 的对话引擎 除了后台自动研判，我们还构建了一个面向运营人员的对话式 AI 助手。它是整个平台的自然语言入口： "最近一小时有多少 Critical 告警？" "分析一下 IP 198.51.100.7 的所有活动" "生成今天的安全运营日报" "查询运营 SOP 中关于勒索软件的处理流程" 对话引擎基于 LangGraph StateGraph 实现，状态图如下： ┌──────────┐ │ START │ └────┬─────┘ ↓ ┌───────────┐ ┌───→│ call_llm │←──────────────┐ │ └─────┬─────┘ │ │ │ │ │ ┌─────┴─────┐ │ │ │ 条件路由 │ │ │ └─────┬─────┘ │ │ ┌────┼────┐ │ │ │ │ │ │ 有tool│ │有 │达到最大│ │ calls│ │文本│轮次 │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ ┌──────────┐ ┌───────┐ ┌┴────────┐ │ │execute │ │finali-│ │force_ │ │ │tools │→│ze │ │text │ │ └──────────┘ └───┬───┘ └────┬────┘ │ │ │ │ ↓ ↓ │ ┌─┴───────────┴─┐ │ │ END │ │ └───────────────┘ │ │ (execute_tools 完成后回到 call_llm)─┘ 关键设计： 最多 8 轮工具调用：防止 LLM 陷入无限循环，第 9 轮强制输出文本总结； 最近 10 轮上下文：通过环境变量 ASSISTANT_MAX_CONTEXT_ROUNDS 控制上下文窗口大小，平衡对话连贯性和 Token 消耗； 冗余工具调用检测：内置 _is_redundant_tool_call() 函数。例如 LLM 调用了 generate_daily_report（该工具内部已经聚合调用了 get_dashboard_stats、security_score、get_alert_trend），Agent 会自动拦截 LLM 后续对这些子工具的重复调用，避免循环浪费； 单例模式：全局唯一的 ChatEngine 实例，所有对话共享 Graph 编译结果，避免重复初始化开销。 6.2 10 大类 40+ Function Calling 工具 对话助手的能力上限，取决于它能调用多少真实世界的工具。我们定义了 10 大类工具： 工具组 工具示例 典型问法 告警查询 query_waf_alerts、query_sysmon_alerts、query_muyun_alerts、query_sangfor_alerts、query_vulnerabilities "最近有什么高危告警？" 资产管理 query_servers、query_domains、query_business_lines "用户中心业务线有哪些服务器？" 安全策略 query_firewall_policies、query_ddos_blacklist、query_muyun_rules "当前防火墙有哪些封禁策略？" 统计分析 get_dashboard_stats、get_alert_trend、get_workorder_stats "本周告警趋势怎么样？" 智能分析 ip_association_analysis、domain_association_analysis、security_score "分析 203.0.113.42 的关联行为" 报告生成 generate_daily_report、generate_weekly_report、generate_handover_summary "生成今天的日报" 安全巡检 security_inspection "执行一次安全巡检" 合规检查 compliance_check "检查一下数据库配置是否符合等保要求" 应急响应 emergency_response "发现勒索软件怎么办？" 知识检索 search_platform_manual、search_all_knowledge "Splunk 怎么查最近被攻击最多的IP？" 每个工具都有细粒度权限控制（TOOL_PERMISSION_MAP）：告警查询类工具需要对应 xxx:read 权限，而统计、分析、报告、知识类工具对所有认证用户开放。超级用户自动获得全部工具。 6.3 动态视角标签 一个容易被忽视但非常实用的设计：AI 回答时会自动标注回答视角： alert（告警研判视角）—— 关注威胁判断、攻击分析 analysis（安全分析视角）—— 关注数据统计、趋势洞察 response（处置响应视角）—— 关注操作建议、应急流程 report（报告视角）—— 关注汇总、总结 compliance（合规审查视角）—— 关注法规符合性 inspection（巡检视角）—— 关注系统健康检查 emergency（应急响应视角）—— 关注止损、溯源、恢复 knowledge（知识问答视角）—— 关注概念解释、操作指南 为什么需要视角标签？ 安全是一个多面体。同样一个问题"分析一下这个 IP"，问的可能是威胁研判，也可能是资产归因。视角标签让运营人员一眼就能判断 AI 的回答"是不是从我想要的角度出发的"，避免"答非所问"的困扰。 6.4 嵌入式应急响应 SOP emergency_response 工具不是简单从知识库检索，而是内置了 4 种应急场景的结构化 SOP： 场景 止损阶段 溯源阶段 恢复阶段 入侵响应 网络隔离 + 账户冻结 日志分析 + 后门排查 系统还原 + 漏洞修补 DDoS 攻击 流量清洗 + 黑洞路由 攻击特征分析 业务恢复 + 容量扩容 数据泄露 阻断外联 + 冻结权限 泄露范围评估 + 路径追踪 通知受影响方 + 加固 勒索软件 立即断网 + 关闭共享 感染源定位 + 变种分析 从备份恢复 + 全面杀毒 每个场景的 SOP 都经过安全团队的专家审核，确保 AI 给出的建议专业可靠。 七、亮点五：RAG 知识增强 —— 让 AI "读懂"安全 7.1 为什么需要 RAG？ 没有领域知识的 LLM，就像一个刚入职但没接受过任何安全培训的新人 —— 他能聊天，但不懂安全。RAG（检索增强生成）让 LLM 查询问题之前先"翻一遍专业书"。 我们构建了 6 大类专业知识库： 知识库 内容 典型问题 Splunk 查询知识 SPL 语句模板、字段映射、数据源说明 "怎么查登录失败最多的 IP？" 安全运营 SOP 告警处置标准流程、升级规范、值班手册 "WAF 告警处置的标准流程是什么？" 合规法规库 GDPR、个人信息保护法、PCI-DSS、等保 2.0 "数据跨境传输有哪些合规要求？" 历史案例库 真实告警的分析记录和处置结果 "SQL 注入告警怎么研判？" CMDB 资产库 服务器 IP、域名、业务线映射、责任人 "核心业务系统有哪些资产？" 安全设备手册 WAF/防火墙/EDR 的操作手册和最佳实践 "怎么在阿里云防火墙上封禁 IP？" 7.2 向量检索流水线 ┌── 查询时 ──┐ │ │ 用户问题 │ ↓ │ 文本向量化 │ (text-embedding-v3 │ 1024维向量) │ ↓ │ Milvus ANN 检索 │ (IVF_FLAT 索引 │ COSINE 相似度) │ Top 20 候选 │ ↓ │ Reranker 精排 │ (qwen3-rerank │ / gte-rerank) │ Top 5 结果 │ ↓ │ 注入 LLM Prompt │ │ ┌── 建库时 ──┐ │ │ │ │ │ 文档上传 │ │ │ ↓ │ │ │ Docling │ │ │ 解析 │ │ │ ↓ │ │ │ 6种切分 │ │ │ 策略 │ │ │ ↓ │ │ │ 向量化 │ │ │ ↓ │ │ │ Milvus │ │ │ 存储 │ │ └────────────┘ │ Milvus 配置： 集合使用 COSINE 相似度度量，更适合语义相似度检索； 索引类型 IVF_FLAT，在召回率和检索速度之间取得平衡； 嵌入模型 text-embedding-v3，输出 1024 维向量，兼顾精度和存储成本。 7.3 六种文档切分策略 —— RAG 质量的生命线 RAG 的效果瓶颈往往不在嵌入模型，而在文档切分。如果一刀切地按 1000 Token 等距切分，很容易把完整的语义单元切碎。我们针对不同文档类型定制了 6 种切分器： ┌───────────────────────────────────────────────────────┐ │ 六种切分策略矩阵 │ ├──────────┬──────────────┬──────────────────────────────┤ │ 策略 │ 适用文档类型 │ 切分逻辑 │ ├──────────┼──────────────┼──────────────────────────────┤ │ clause │ 法规条款 │ 按 ## 二级标题切分 │ │ │ (GDPR等) │ 每个条款一个独立 chunk │ │ │ │ chunk_size=5000 │ ├──────────┼──────────────┼──────────────────────────────┤ │ structu- │ 结构化文档 │ 按 #/##/### 三级标题切分 │ │ red │ (技术手册等) │ 保留标题层级关系 │ │ │ │ chunk_size=1000 │ ├──────────┼──────────────┼──────────────────────────────┤ │ markdown │ Markdown │ LangChain 语言感知切分 │ │ │ 技术文档 │ 保护代码块/表格/列表完整性 │ │ │ │ chunk_size=1000 │ ├──────────┼──────────────┼──────────────────────────────┤ │ token │ 通用文本 │ 按 Token 数精确切分 │ │ │ │ chunk_size=1024 tokens │ ├──────────┼──────────────┼──────────────────────────────┤ │ semantic │ 无结构文档 │ SemanticChunker │ │ │ (会议纪要等) │ 按语义相似度断点切分 │ │ │ │ percentile=95 │ ├──────────┼──────────────┼──────────────────────────────┤ │ plain │ 纯文本 │ RecursiveCharacterTextSplitter│ │ │ │ 按自然分隔符递归切分 │ └──────────┴──────────────┴──────────────────────────────┘ 为什么切分策略如此重要？ 举个直观的例子： 一条 GDPR 第 32 条关于"处理安全性"的条款，原文可能是一段 2000 字的连贯法律文本。如果按 Token 等距切分，恰好在第 1200 Token 处切断，那么： chunk_A："...控制者应当实施适当的技术和组织措施以确保..." chunk_B："...违反适当安全等级的情况下的应对措施..." 当用户搜索"GDPR 安全措施要求"时，embedding 模型可能只匹配到 chunk_A，但 chunk_A 只说了"应该实施"，没有说"怎么实施、实施到什么程度" —— 答案在第 1201 Token 之后的 chunk_B 里，但用户永远看不到。 使用 clause 策略按条款号切分后，整条第 32 条是一个完整的 chunk，检索时不会被截断，召回完整性大幅提升。 7.4 多知识库智能路由 当平台有 6 个知识库时，不可能每次查询都搜全部库（成本高、噪音大）。MultiRAGEngine 引入了 KnowledgeRouterAgent： 用户问题："Splunk怎么查SQL注入攻击？" ↓ KnowledgeRouterAgent（轻量LLM调用）分析问题意图 ↓ 判断知识域: splunk_queries（概率最高） ↓ 仅检索 splunk_queries 知识库 ↓ 返回专业的 SPL 查询模板 路由 Agent 先判断问题属于哪个知识域，然后只检索最相关的 1-2 个知识库，避免跨域检索引入的噪音。 八、亮点六：合规审查 4-Agent 流水线 8.1 场景与痛点 企业在上线新产品、签订新合同、采购第三方服务时，需要确保内容符合相关法律法规要求。传统做法是法务人员/安全合规专家逐条比对法规： 一份 50 页的产品协议，需要花费 4-6 小时逐条审查； 涉及多法域（如同时面向中国、欧盟、美国用户），工作量翻倍； 人工审查容易遗漏条款，尤其是非核心法域的法规。 我们的合规审查模块用 4-Agent 流水线将这个过程压缩到 15 分钟。 8.2 4-Agent 流水线 ┌──────────────────────────────────────────────────────────────┐ │ 合规审查 4-Agent 流水线 │ │ │ │ 用户上传文档 + 选择法域（中国/欧盟/美国/日本/韩国/新加坡/国际） │ │ ↓ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Agent 1: 文档理解 (Docling 解析 + 条款提取) │ │ │ │ • 解析 PDF/DOCX/MD 等格式 │ │ │ │ • 提取文档结构（章节标题、条款编号） │ │ │ │ • 输出：条款清单 [{"编号": "3.2", "内容": "..."}] │ │ │ └──────────────────────────┬───────────────────────────┘ │ │ ↓ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Agent 2: 条款精排 (RAG 检索 + Reranker) │ │ │ │ • 将文档条款向量化 │ │ │ │ • 在所选法域的法规知识库中检索相关法规条款 │ │ │ │ • Reranker 精排候选法规（Top 5） │ │ │ │ • 输出：每条款匹配的法规候选集 │ │ │ └──────────────────────────┬───────────────────────────┘ │ │ ↓ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Agent 3: 逐条审查（深度 LLM 比对） │ │ │ │ • 将文档条款 vs. 法规条款逐条交给 LLM 比对 │ │ │ │ • LLM 判断：合规 / 不合规 / 部分合规 / 不适用 │ │ │ │ • 输出：findings[] = [{条款, 状态, 风险描述, 修改建议}] │ │ │ └──────────────────────────┬───────────────────────────┘ │ │ ↓ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Agent 4: 汇总报告 (结构化报告生成) │ │ │ │ • 汇总所有 findings │ │ │ │ • 按风险等级排序 │ │ │ │ • 生成总体合规评分 │ │ │ │ • 输出：结构化审查报告（前端渲染） │ │ │ └──────────────────────────┬───────────────────────────┘ │ │ ↓ │ │ Validator 数据校验 │ │ → 写回数据库 │ │ → 前端展示审查报告 │ └──────────────────────────────────────────────────────────────┘ 为什么是 4 个 Agent 而不是 1 个？ 这是"Divide and Conquer"策略在 LLM 应用中的典型实践： 单一职责：每个 Agent 只做一件事，提示词更容易优化，行为更可控； 独立调试：Agent 3 的审查质量不行？只调 Agent 3 的提示词，不影响其他环节； 并行化可能：Agent 3 的逐条审查可以并行执行（如果条款数量多），大幅缩短总耗时； 成本和精度的平衡：Agent 2 做轻量级粗筛（RAG），Agent 3 做重量级精判（LLM），避免全量条款都走 LLM。 8.3 多法域支持 平台内置了 7 个法域的法规库： 法域 典型适用法规 中国 (CN) 个人信息保护法、数据安全法、网络安全法、等保 2.0 欧盟 (EU) GDPR、Digital Services Act 美国 (US) CCPA/CPRA、HIPAA、PCI-DSS 日本 (JP) 個人情報保護法 (APPI) 韩国 (KR) 個人情報保護法 (PIPA)、信用情報法 新加坡 (SG) PDPA 国际 ISO 27001、SOC 2 用户只需在下拉框中选择目标法域，RAG 系统会自动在该法域的法规知识库中检索匹配条款。新增法域只需导入对应法规文档到知识库即可。 8.4 人工复核闭环 AI 的审查结果不是最终结论。平台支持人工复核反馈： 审查报告中的每个 finding 旁边都有反馈按钮： [✅ AI判断正确] [❌ AI判断有误] [📝 补充说明] 反馈数据回写 ComplianceFeedback 表： - 用于持续优化审查 Agent 的提示词 - 积累高质量 Few-shot 样本 - 统计各法域的 AI 审查准确率 九、亮点七：安全大屏与态势感知 9.1 实时安全态势 大屏不仅仅是"好看"，它承担着几个关键职能： 管理层视角：一眼看懂"今天安全吗？"； 值班视角：实时告警滚动，异常波动立即可见； 运营视角：处置效率统计，找到流程瓶颈。 9.2 大屏数据维度 面板 数据内容 更新频率 技术实现 攻击来源地图 基于 GeoLite2 的实时攻击 IP 地理分布 30秒 Redis 缓存 + 前端轮询 告警趋势图 各类型告警的时间曲线、同比环比 5分钟 数据库定时快照 威胁等级分布 Critical/High/Medium/Low 饼图 实时 MySQL 聚合查询 处置统计 今日封禁 IP 数、平均处置响应时间 实时 审计日志聚合 资产风险面板 漏洞 Top 10、暴露端口统计、未修复补丁 1小时 定时任务扫描 AI 研判统计 AI 准确率、误报率趋势、日均分析量 1小时 人工反馈聚合 性能优化策略： Redis 缓存 + 定时快照：高频查询的数据（如攻击地图坐标）缓存到 Redis，30 秒刷新一次，避免频繁查询数据库； 前端 ECharts 懒加载：不同面板独立加载，不阻塞页面渲染； WebSocket 推送：Critical 级别告警通过 WebSocket 实时推送到大屏，无需轮询。 十、技术架构全景 10.1 系统架构 ┌─────────────────────────────────────────────────────────────────┐ │ 用户交互层 │ │ │ │ ┌──────────┐ ┌─────────────┐ ┌────────────────────────┐ │ │ │ Vue 3 前端│ │ 飞书客户端 │ │ REST API (第三方集成) │ │ │ │ Element+ │ │ 卡片/通知 │ │ JWT 认证 │ │ │ │ ECharts │ │ 按钮回调 │ │ │ │ │ └────┬─────┘ └──────┬──────┘ └───────────┬────────────┘ │ │ │ │ │ │ ├────────┼────────────────┼───────────────────────┼────────────────┤ │ │ 业务处理层 │ │ │ │ │ │ │ ┌────┴────────────────────────────────────────┴──────────┐ │ │ │ Django 5.2 + DRF │ │ │ │ ┌──────────┬──────────┬──────────┬─────────────────┐ │ │ │ │ │ 告警API │ 对话API │ 合规API │ 管理API │ │ │ │ │ └──────────┴──────────┴──────────┴─────────────────┘ │ │ │ └──────────────────────┬─────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────┴─────────────────────────────────┐ │ │ │ Celery 异步任务调度 (Redis Broker) │ │ │ │ ┌───────┬───────┬───────┬───────┬───────┬──────────┐ │ │ │ │ │ waf │sysmon │ muyun │ dlp │sangfor│compliance│ │ │ │ │ │ queue │ queue │ queue │ queue │ queue │ queue │ │ │ │ │ └───────┴───────┴───────┴───────┴───────┴──────────┘ │ │ │ │ Celery Beat (30秒定时扫描) │ │ │ └──────────────────────┬─────────────────────────────────┘ │ │ │ │ ├──────────────────────────┼──────────────────────────────────────┤ │ AI 智能层 │ │ │ │ ┌──────────────────────┴─────────────────────────────────┐ │ │ │ DashScope (DeepSeek-V4-Pro / Qwen-Max) │ │ │ │ │ │ │ │ ┌──────────────────────┐ ┌────────────────────────┐ │ │ │ │ │ LangGraph ChatEngine │ │ 专用 LLM Agent 矩阵 │ │ │ │ │ │ • 状态图多轮对话 │ │ • WafAgent │ │ │ │ │ │ • 40+ FunctionCalling │ │ • SysmonAgent │ │ │ │ │ │ • SSE 流式输出 │ │ • MuyunHostAlertAgent │ │ │ │ │ │ • 动态视角标签 │ │ • SangforAgent │ │ │ │ │ └──────────────────────┘ │ • SecurityActionAgent │ │ │ │ │ │ • AnalyzerAgent │ │ │ │ │ ┌──────────────────────┐ │ • ComplianceReviewer │ │ │ │ │ │ RAG 知识增强 │ └────────────────────────┘ │ │ │ │ │ • MultiRAGEngine │ │ │ │ │ │ • 知识路由 Agent │ ┌────────────────────────┐ │ │ │ │ │ • 6种切分策略 │ │ ToolRegistry │ │ │ │ │ │ • Reranker 精排 │ │ 安全设备工具统一注册 │ │ │ │ │ └──────────────────────┘ └────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ ├──────────────────────────────────────────────────────────────────┤ │ 数据存储层 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │ │ MySQL │ │ Redis │ │ Milvus │ │ Splunk │ │ │ │ 业务数据 │ │ 缓存+队列 │ │ 向量知识库│ │ 日志平台 │ │ │ │ RBAC权限 │ │ Session │ │ 6知识库 │ │ 原始告警日志 │ │ │ └──────────┘ └──────────┘ └──────────┘ └───────────────┘ │ │ │ ├──────────────────────────────────────────────────────────────────┤ │ 安全设备层 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ ┌──────────┐ │ │ │阿里云 │ │阿里云 │ │牧云 │ │腾讯云│ │飞塔 │ │ │ │防火墙 │ │DDoS高防 │ │EDR │ │WAF │ │WAF │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────┘ └──────────┘ │ └──────────────────────────────────────────────────────────────────┘ 10.2 关键技术选型理由 技术 用途 为什么选它 Django 5.2 Web 框架 ORM 强大、生态成熟、适合复杂业务建模和快速迭代 Vue 3 + Element Plus 前端 组件化开发效率高、Element Plus 企业级组件库开箱即用 Celery + Redis 异步任务 稳定可靠的多队列支持、丰富的监控工具（Flower） LangGraph 对话引擎 有向状态图天然适合多轮对话 + Tool Calling 的场景 Milvus 向量数据库 十亿级向量检索性能、COSINE 相似度、分布式可扩展 DashScope LLM 平台 中文安全领域表现优秀、Function Calling 能力强、API 稳定 Qwen3-Rerank 重排序 中文语义理解精准、大幅提升 RAG 召回精度 Docling 文档解析 准确保留表格、章节结构，远优于传统 PDFMiner 飞书 lark_oapi 消息通知 WebSocket 持久化连接、卡片交互丰富、深度打通组织通讯录 10.3 部署架构 生产环境 (Gunicorn)： Nginx (反向代理) │ ┌───┴────────────────────────────┐ │ Gunicorn (1 worker, 4 threads)│ │ 端口 8000 │ └───┬────────────────────────────┘ │ ┌───┴────────────────────────────┐ │ Celery Worker (6 队列, 10并发) │ │ Celery Beat (定时任务调度) │ │ Celery Flower (任务监控, 5555) │ └───┬────────────────────────────┘ │ ┌───┴────────────────────────────┐ │ MySQL + Redis + Milvus │ │ (外部服务, TCP 连接) │ └────────────────────────────────┘ 十一、关键挑战与工程实践 11.1 LLM 输出稳定性 挑战：Agent 要求输出严格 JSON，但 LLM 有时会输出多余的解释文本、缺失字段、或 JSON 格式不合法。 解决方案： 多层约束：System Prompt 明确定义 JSON Schema → 正则提取 JSON 块 → json.loads() 解析 → 启发式修复 → Schema Validator 校验 → 失败重试（最多 2 次）； 温度控制：需要结构化输出的场景 temperature=0.1，需要创意的对话场景 temperature=0.3； 输出格式约定：约定 JSON 必须包裹在 ```json 代码块中，否则正则无法准确定位。 11.2 Token 成本控制 挑战：日均数千条告警，如果每条都带完整上下文调用 LLM，成本难以接受。 优化策略： 优化手段 具体做法 降幅 批量分析 5-20 条同类告警一次调用分析 60-80% 上下文裁剪 Payload 截取前 500 字符，日志只保留关键行 30-50% SPL 缓存 相同查询条件在 5 分钟内命中 Redis 缓存 20-30% 预过滤 明确误报在进入 LLM 前就过滤掉 15-25% 分层模型 简单告警用 qwen-turbo，复杂告警用 deepseek-v4-pro 40-60% 综合优化后，日均 Token 消耗控制在可接受范围内。 11.3 跨源告警关联 挑战：攻击者的完整攻击链可能同时触发 WAF、Sysmon、DLP 三条告警，如果各自由不同 Agent 独立处理，无法还原攻击全貌。 解决方案：设计了 AlertContext 协议，在 Agent 分析时自动补充跨源上下文： WAF 告警的 Agent 分析时： → 自动查询：同一源 IP 在 Sysmon 中的终端行为 → 自动查询：同一源 IP 在 DLP 中的数据外传记录 → 自动查询：威胁情报中的 IP 信誉 → 自动查询：目标在 CMDB 中的资产重要性 这让 AI 看到的不是孤立的"一条 WAF 告警"，而是一个"攻击者行为画像"。 11.4 处置安全性 挑战：从飞书卡片一键封禁 IP 是高风险操作 —— 误触怎么办？越权怎么办？ 多重防护： 二次确认：Critical 操作（如封禁、隔离主机）弹出确认对话框，需二次点击； RBAC 权限：飞书用户 → 平台用户 → 角色权限，无权限用户点击按钮即拒绝； 全链路审计：auto_audit 装饰器自动记录每次处置操作（操作人、时间、对象、结果、原始告警）； 时长上限：临时封禁有最大时长限制，防止误设永久封禁； 回滚能力：每次封禁操作同时生成 Redis Token，30 天内可一键解封。 十二、上线效果 平台上线运行一个月以来，核心指标改善显著： 指标 上线前 上线后 改善幅度 告警平均响应时间 (MTTR) 45 分钟 5 分钟 ↓ 89% 单人日均处理告警量 200 条 2000+ 条 ↑ 10x 告警误报率（AI + 人工复核） ~30% <5% ↓ 83% 单次处置操作耗时 5-10 分钟 3-5 秒 ↓ 99% 合规审查（单份文档） 4-6 小时 15 分钟 ↓ 95% 新员工上手周期 2-3 周 2-3 天 ↓ 85% 7×24 覆盖（无需夜班人力） ❌ ✅ - 更重要的是质的改变： 安全分析师的日常工作从"看告警、判误报"变成了"看 AI 报告、做决策"； 初级工程师借助 AI 助手能独立完成原本需要高级工程师的经验才能完成的研判； 合规审查从"一个法务全包"变成了"AI 初筛 + 人复核"，大幅释放高端人力。 十三、心得体会 13.1 做对了什么 ① Prompt Engineering 是第一生产力 一个精心调校的 System Prompt，比调整模型参数有效 10 倍。我们的 WAF Agent 提示词迭代了 12 个版本，从最早的"你是安全专家，分析这个告警"到后来包含攻击模式分类表、编码识别规则、可信度评估框架，每次迭代都带来可感知的研判质量提升。 ② 单一职责 Agent > 万能 Agent 不要试图用一个 Agent 做所有事。把任务拆解为多个专职 Agent，每个只做一件事。这不仅是软件工程的"高内聚、低耦合"原则，在大模型应用中也同样成立 —— 单一职责的 Agent 提示词更短、行为更可控、调试更容易。 ③ 文档切分是 RAG 质量的生命线 投入在文档切分策略优化上的时间，回报远大于换一个更贵的嵌入模型。针对不同文档类型（法规、技术手册、操作指南）定制切分策略，把检索召回率从约 60% 提升到了 90% 以上。 ④ 人机协同比全自动更可靠 AI 做研判、提建议，人类做决策、点确认。Critical 级别的操作永远保留人工确认环节。这不是"AI 能力不足"，而是"安全运营责任重大"。审计日志告诉我们：谁、什么时候、做了什么、为什么 —— 没有这四件事，自动化等于蒙眼狂奔。 ⑤ 可观测性是 AI 系统的地基 当 AI 开始操作你的防火墙，你必须能回答： 这个操作是哪个 Agent 决策的？ 决策依据是什么（告警上下文、Prompt、LLM 返回）？ 操作结果成功还是失败？ 谁点下了确认按钮？ 从第一天就做好日志、审计、监控，否则当 AI 做出一个错误决策时，你甚至无法回溯原因。 13.2 未来规划 Agentic Workflow 深化：从单步分析演进到"Plan → Execute → Observe → Reflect"多步推理，让 AI 自主完成完整的安全调查链路； 多模态告警分析：支持截图、网络拓扑图、安全设备控制台截图的分析； 安全运营 Copilot：从被动响应到主动巡检，AI 定时扫描资产暴露面、基线合规、弱口令等风险； 攻击路径自动还原：利用 Graph 技术自动拼接跨设备日志，还原完整的攻击时间线； AIOps 集成：安全告警与 IT 运维告警联动，区分"攻击"和"故障"。 十四、技术选型决策记录 15.1 为什么自建而不是买商业 SOAR？ 维度 商业 SOAR（如 Splunk Phantom、XSOAR） 自建平台 AI 原生能力 Playbook 是 if-else 逻辑，无 LLM 研判 LLM 自主决策，弹性应对未知攻击 定制灵活性 受限于产品 API 和 Playbook 语法 完全自主，想怎么改怎么改 自然语言交互 无 40+ 工具链的对话式交互 合规审查 无 内置 4-Agent 审查流水线 成本 License ¥50-200万/年 开发 + 运维 + LLM API ≈ ¥15-30万/年 维护复杂性 低 中（需要自己的开发团队） 结论：商业 SOAR 的 Playbook 模式无法满足我们对 LLM 研判和弹性决策的需求。自建虽然前期投入更大，但在 AI 原生能力和长期迭代灵活性上有压倒性优势。 15.2 为什么用 LangGraph 而不是自己写循环？ 在对话引擎的实现上，有三个选项： 手写 while 循环：最灵活，但状态管理、边界条件处理、并发控制全部要自己写； LangChain AgentExecutor：开箱即用，但黑盒程度高，难以定制"达到最大轮次后强制文本回复"这种逻辑； LangGraph StateGraph：可自由定义节点和条件路由，状态流转清晰，调试友好，同时保留了手写循环的灵活性。 我们选择了 LangGraph，唯一的代价是学习它的 State 不可变模型（踩过一次坑）。但换来的是： 节点逻辑单一、可独立测试； 条件路由一目了然（add_conditional_edges）； 新增节点（如一键回滚、多轮澄清）只需要加边，无需改核心逻辑。 十六、AI 研判质量评估体系 "你们这个 AI 准确吗？"— 这是每个来访者都会问的问题。如何回答这个问题，需要一套科学的评估体系。 16.1 评估指标设计 指标 定义 目标值 当前值 研判准确率 AI 给出的威胁等级与人工复核一致的比率 >90% 94.2% 误报率 AI 判为 Critical/High 但人工确认为误报的比例 <5% 3.8% 漏报率 AI 判为 Low/Medium 但人工确认为真实威胁的比例 <2% 1.1% 处置建议采纳率 运营人员直接采纳 AI 建议（不改参数）的比例 >70% 81.5% JSON 有效率 Agent 输出能成功解析为合法 JSON 的比例 >95% 98.2% 16.2 评估流程 ┌─ 自动化评估（每日）─────┐ ┌─ 人工抽样评估（每周）──┐ │ │ │ │ │ 每日抽取 100 条告警 │ │ 每周随机抽取 200 条 │ │ ↓ │ │ ↓ │ │ 高级分析师逐条标注 │ │ 多分析师交叉验证 │ │ （ground truth） │ │ （Cohen's Kappa>0.8） │ │ ↓ │ │ ↓ │ │ 与 AI 结果自动比对 │ │ 计算各项指标 │ │ ↓ │ │ ↓ │ │ 生成日报指标 │ │ 生成周报 + 趋势图 │ └─────────────────────────┘ └───────────────────────┘ 16.3 持续改进机制 反馈闭环：飞书卡片上的每一个 AI 研判结果旁边，都有一个不起眼但至关重要的 👎 按钮。 运营人员点击 👎 → 选择原因： [ ] AI 威胁等级判断偏高 [ ] AI 威胁等级判断偏低 [ ] AI 分析逻辑有误 [ ] AI 建议不切实际 [ ] 其他：___ → 反馈写入 ChatFeedback 表 → 每周自动生成"AI 误判 Top 5 模式"报告 → 针对性优化提示词或 Few-shot 示例 版本 A/B 测试：每次提示词重大变更，先在影子模式运行 3 天，新版本 Agent 的研判结果与当前版本对比，只有当关键指标（准确率、误报率）显著提升时才上线。 16.4 为什么我们没有追求 100% 准确率？ 因为安全运营的本质不是"判断题"，而是"优先级排序"。 即便 AI 把 10 条 Low 告警误判为 Medium，运营人员也只是多看 10 条，不会导致安全事故。但如果 AI 把 1 条 Critical 漏判为 Low，那就可能错失关键处置窗口。 所以我们的评估体系采取了不对称惩罚： 误报（Low→High）的惩罚权重为 1； 漏报（High→Low）的惩罚权重为 10。 这引导我们不断优化 Prompt 中关于威胁等级定义的部分，确保 AI 对"可疑但不确定"的情况倾向于升级而非降级。 十七、成本分析与 ROI 技术博客往往忽略成本，但任何一个需要向上级申请资源的项目，成本分析都是绕不过的话题。 17.1 LLM API 成本 场景 模型 日均调用量 均价 日均成本 告警研判（WAF） deepseek-v4-pro ~150 批次（每批 10-20 条） ¥4/百万 tokens ¥12 告警研判（Sysmon/牧云等） deepseek-v4-pro ~80 批次 ¥4/百万 tokens ¥8 对话助手 deepseek-v4-pro ~200 次 ¥4/百万 tokens ¥6 RAG 嵌入 text-embedding-v3 ~500 次 ¥0.7/百万 tokens ¥0.5 RAG 重排序 qwen3-rerank ~300 次 ¥2/百万 tokens ¥1 合规审查 deepseek-v4-pro ~5 次 ¥4/百万 tokens ¥2 LLM API 日均成本：约 ¥30，月均约 ¥900。 17.2 基础设施成本 资源 规格 月成本 应用服务器 4C8G × 2（Gunicorn + Celery Worker） ¥800 MySQL 云数据库 RDS 2C4G ¥500 Redis 云数据库 Redis 2G ¥300 Milvus 自建 4C8G（Standalone） ¥600 OSS 存储 文档/日志，50GB ¥30 基础设施月成本：约 ¥2,230。 17.3 ROI 分析 维度 人力节省估算 告警研判 原先 2 名专职分析师，现 1 名复核即可 → 节省 1 人 告警处置 单次操作从 5-10 分钟降到 3-5 秒 → 日均节省 4-6 工时 合规审查 单份文档从 4-6 小时降到 15 分钟 → 月均节省 40+ 工时 新人培训 上手周期从 2-3 周降到 2-3 天 → 节省带教工时 按安全行业平均薪资折算，平台年化 ROI 约 8-10 倍。 17.4 降本经验 批量分析是最大的省钱手段：逐条分析 vs 批次分析，Token 消耗差 3-5 倍； SPL 缓存很重要：相同查询条件 5 分钟内不重复调 Splunk，也间接减少了传递给 LLM 的 Token； 提示词也有 Token 成本：92KB 的 System Prompt 每次调用都要发送。我们通过分层提示词（基础 Prompt 固定 + 动态注入关键上下文）优化后，日均节省约 ¥5； 非高峰期降级：夜间 0-6 点，非 Critical 告警使用 qwen-turbo（比 deepseek-v4-pro 便宜 10 倍），白天人工复核。 十八、写在最后 构建这个平台的半年多时间里，我最大的感悟是：AI 不会取代安全分析师，但会用 AI 的安全分析师一定会取代不会用 AI 的。 大语言模型给安全运营带来的，不是"自动回复几个告警"那么简单。它更像一个支点，撬动了告警研判、知识沉淀、自动化处置、合规审查等一整条运营链路的重构。 当安全设备会"说话"、告警能"自述病情"、处置可以"一键完成"、知识库能"随问随答"，安全运营才能真正从"消防队"升级为"免疫系统"。 如果你也在做类似的事情，或者对文中的任何技术细节感兴趣，欢迎交流讨论。安全运营的智能化之路，我们都在路上
RAG RAG简述 1、RAG的作用：在使用通用模型的时候，可能会存在一些专业领域的知识盲区，导致AI在上下文的时候得不到参考材料，从而出现胡说八道的幻觉。RAG为了解决这种情况，让模型通过内部知识记忆与RAG提供来自外部知识库的非参数化知识结合，使得模型在生成文本前，能通过检索从外部知识库中获取到相关信息，并将这些参考资料融入到生成过程中达到更准确的效果。 2、RAG通过以下的阶段来实现类似于检索结合内部与外部知识： 通过嵌入模型（Embedding Model）将外部知识库编码为索引存到向量数据库中 当用户发起查询时，用同样的嵌入模型将问题向量化，并根据相似度从向量数据中锁定跟问题有关的文档片段。 得到检索阶段送来的相关文档片段和原始问题后，遵循预设的Prompt指令，将将上下文与问题有效整合引导大模型进行准确的响应。 3、RAG的优势： 在使用模型的时候，当模型的响应不满足要求的时候，一般会采用一些方案来让模型的响应更符合自己的需求，一般是如下三种 Prompt工程：精心设计提示词来引导模型，适合模型已有相关知识的场景。 RAG：当模型缺乏特定的专业知识无法回答，使用RAG为其提供相关非参数化知识的上下文信息。 模型微调：当需要改变模型的行为和风格，微调才是最终的选择，能将复杂的指令蒸馏进模型的权重中。 4、RAG的关键优势： 准确度提升：相比较于Prompt工程，RAG能解决了模型预训练知识的限制，补充了模型的知识盲区，且RAG生成的内容在提供的外部知识库中都是可以找到的，具备可信度。 时效性：模型在训练完成后会存在知识滞后的问题，RAG通过引入外部知识库能够进行动态更新，使得模型知识能够得到实时知识的补充。 性比价：相比于微调，RAG外部知识库的强力辅助不需要重新对模型进行训练，避免了算力成本，并且在处理特定领域问题时，可以通过RAG的补充，使用参数量更小的基础模型进行推理，降低了推理的成本。 如何快速构建？ 数据准备：将外部知识库如PDF、Word、Markdown等多源头的数据变成标准化的数据，采用合理的分块策略进行切割。 索引构建：将切分好的文本通过嵌入模型转化为向量，并存入数据库。 检索优化：采用混合检索（向量+关键词）等方式来提升召回率，并引入重排序模型对检索结果进行二次精选。 Prompt生成：设计清晰的Prompt模板，引导 LLM 基于检索到的上下文回答用户问题。 数据加载 RAG系统中，数据加载是通过文档加载器将各种不同格式的文档转换成程序可处理的结构化数据，一般有各种不同的文档加载器，一般文档加载器需要将内容提取成纯文本，同时在解析过程中同时抽取文档来源、页码、作者等关键信息作为元数据，再将文本和元数据整理成统一的数据结构，方便后续切割入库。 主流的文档加载器； 加载器名称 处理格式 特点 TextLoader 基础文本文件加载 纯文本处理 DirectoryLoader 批量目录文件处理 混合格式文档 Unstructured 多格式文档解析 PDF、Word、HTML等 CSVLoader CSV文档解析 .csv WebBaseLoader 网页 URL 网页内容解析 FireCrawlLoader 网页内容抓取 实时网页内容抓取 LlamaParse PDF 深度PDF结构解析 Docx2txtLoader .docx doc文档解析 JSONLoader .json JSON解析器 以Unstructured为例，能识别的文档元素如下： 元素类型 定义 Title 具有标题特征的文本 Paragraph 连续、无列表 / 标题特征的正文文本 UncategorizedText 未分类的自由文本 ListItem 有序 / 无序列表的条目 Table 行列结构化的表格内容 NarrativeText 兜底文本类型：无法归为标题 / 段落 / 列表的零散文本 Header 页面顶部的重复文本 Footer 页面底部的重复文本 PageBreak 标识文档的分页位置 SectionHeader 比Title更细分的章节头部文本 Footnote 页面底部针对特定内容的注释 Image 文档中的图片元素 Audio 文档中的音频嵌入对象 EmbeddedFile 文档中嵌入的附属文件 Code 带语法特征的代码片段 FigureCaption 图片 / 图表下方的说明文本 Address/Phone/EmailAddress 邮箱、地址 PageBreak 页面分隔符 PageNumber 页码 示例代码： from unstructured.partition.auto import partition from collections import Counter # -------------------------- 配置参数 -------------------------- pdf_path = "TEST.pdf" # 你的PDF路径 # 中文解析关键参数：简体中文+英文（适配中文PDF/OCR） OCR_LANGUAGES = "chi_sim+eng" # 文本截断长度（避免长文本刷屏，可根据需要调整） TEXT_TRUNCATE_LEN = 300 # 编码格式（解决中文乱码） ENCODING = "utf-8" # -------------------------- 核心解析逻辑 -------------------------- def parse_pdf_with_unstructured(pdf_path): try: # 3. 解析PDF（补充中文适配参数） elements = partition( filename=pdf_path, content_type="application/pdf", ) # 4. 过滤空元素（避免统计无效内容） elements = [elem for elem in elements if elem.text.strip()] return elements except Exception as e: print(f"❌ PDF解析失败：{str(e)}") return None # -------------------------- 执行解析并输出结果 -------------------------- if __name__ == "__main__": # 解析PDF elements = parse_pdf_with_unstructured(pdf_path) # 1. 精准统计（改用element.text，避免元数据干扰） total_elements = len(elements) total_chars = sum(len(elem.text.strip()) for elem in elements) print(f"✅ 解析完成: {total_elements} 个有效元素, 总字符数: {total_chars}") # 2. 统计元素类型（按数量降序排列） type_counter = Counter(e.category for e in elements) print(f"\n📊 元素类型分布: {dict(type_counter.most_common())}") # 3. 友好显示所有元素（截断长文本） print("\n📝 所有元素详情:") print("-" * 80) for i, element in enumerate(elements, 1): # 截断过长的文本，保留关键内容 display_text = element.text.strip() if len(display_text) > TEXT_TRUNCATE_LEN: display_text = display_text[:TEXT_TRUNCATE_LEN] + "..." print(f"Element {i} ({element.category}):") print(element) print(f"内容: {display_text}") print("-" * 80) 文本分块 1、文本分块是将数据加载出来的长篇文档，切分成小块且易处理的小文本块，由于嵌入模型有严格的输入长度（Token）限制，文本块的大小必须小于等于嵌入模型的上下文窗口，同时由于LLM同样有上下文窗口限制，与用户的问题和提示放入的时候，需要尽可能容纳的块要很相关，因此分块后的文本块的质量非常重要。 2、如何定义分出来的一个文本块是质量高的？ LLM就回答的过程中，当文本块很长，LLM会充满大量的上下文使得关键信息被稀释，从而导致型就很难从中提取出最关键的信息来形成答案。因此一个好的文本块主题是明确且一致的。 3、嵌入的过程中，文本内容是被压缩成向量的，因此信息损失是不可避免的，以Transformer编码器的压缩过程为例： 分词 (Tokenization): 将文本拆成模型的最小单元（Token） 向量化 (Vectorization): Transformer 为每个 token 生成一个高维向量表示（768/1024 个数字组成的数组）。 池化 (Pooling): 将所有 token 的向量压缩成一个单一的向量，这个向量代表了整个文本块的语义(平均池化、[CLS]位取向量等）。 以RMIConnector为例 分词：[CLS]、R、M、I、C、o、n、n、e、c、t、o、r、[SEP] 向量化：CLS 模拟成[0.12, -0.35, 0.28, 0.41, -0.19]类似的向量，预训练时代表整句语义 ​ R模拟成[0.15, -0.32, 0.29, 0.38, -0.21]和 M/I 语义关联，向量数值接近。 池化：平均池化：第1维：(0.12+0.15+..+..)/n得到平均维 分块策略 固定分块：默认采用\n\n分割符，使用正则表达式将文本按段落进行分割，交给_merge_splits函数进行智能合并，按顺序添加片段，并实时计算累计长度，当超过了chunk_size，触发生成一个块。生成块后，从头部移除片段，保留尾部chunk_overlap，作为下一个块的开始，实现重叠。 class CharacterTextSplitter(TextSplitter): """Splitting text that looks at characters.""" def __init__( self, separator: str = "\n\n", is_separator_regex: bool = False, **kwargs: Any, ) -> None: """Create a new TextSplitter.""" super().__init__(**kwargs) self._separator = separator self._is_separator_regex = is_separator_regex def split_text(self, text: str) -> list[str]: sep_pattern = ( self._separator if self._is_separator_regex else re.escape(self._separator) ) splits = _split_text_with_regex( text, sep_pattern, keep_separator=self._keep_separator ) lookaround_prefixes = ("(?=", "(?<!", "(?<=", "(?!") is_lookaround = self._is_separator_regex and any( self._separator.startswith(p) for p in lookaround_prefixes ) merge_sep = "" if not (self._keep_separator or is_lookaround): merge_sep = self._separator return self._merge_splits(splits, merge_sep) 递归分块： 遍历分隔符列表，找到文本中第一个存在的分隔符作为当前分割依据，并记录剩余更低优先级分隔符。 用转义后选定的分隔符分割文本，得到初始的分割结果 遍历初始分割结果 splits，逐个检查片段大小是否符合 chunk_size： 小片段：暂存到 good_splits，后续合并成完整块。 超大片段：先处理已暂存的小片段（合并后加入最终结果），再判断是否能递归分割： 无剩余分隔符：直接保留超大片段。 有剩余分隔符：用更低优先级分隔符递归调用 _split_text** 分割该片段，结果加入最终列表。 遍历完所有初始片段后，若 good_splits 还有未处理的小片段，合并后加入最终结果。 def _split_text(self, text: str, separators: list[str]) -> list[str]: final_chunks = [] separator = separators[-1] new_separators = [] for i, _s in enumerate(separators): separator_ = _s if self._is_separator_regex else re.escape(_s) if not _s: separator = _s break if re.search(separator_, text): separator = _s new_separators = separators[i + 1 :] break separator_ = separator if self._is_separator_regex else re.escape(separator) splits = _split_text_with_regex( text, separator_, keep_separator=self._keep_separator ) good_splits = [] separator_ = "" if self._keep_separator else separator for s in splits: if self._length_function(s) < self._chunk_size: good_splits.append(s) else: if good_splits: merged_text = self._merge_splits(good_splits, separator_) final_chunks.extend(merged_text) good_splits = [] if not new_separators: final_chunks.append(s) else: other_info = self._split_text(s, new_separators) final_chunks.extend(other_info) if good_splits: merged_text = self._merge_splits(good_splits, separator_) final_chunks.extend(merged_text) return final_chunks 语义分块：SentenceTransformersTextSplitter 是语义驱动的文本分割器，会通过模型生成每个单元的语义嵌入向量，计算相邻单元的语义相似度，最终将语义相近的单元合并成符合 chunk_size 要求的块，大致流程如下： 默认按自然语言的句子分隔符拆分，得到一个个独立的小句子/分句作为语义单元。 对于最小的语义单元，调用SentenceTransformers模型生成嵌入向量，向量在高维空间中的距离越近；语义差异越小。 计算每两个相邻语义单元的余弦相似度（或欧氏距离）：相似度越接近 1，语义越相关；越接近 0，语义越无关，将相似度转换成将相似度转换为差异度，说明两个单元的语义关联越弱，是最优分割点。 合并语义单元，持续向列表中添加下一个相邻单元，同时计算合并后的总长度： 若总长度 未超过 chunk_size：继续添加下一个单元。 若总长度 即将超过 chunk_size：检查当前列表最后一个单元与下一个单元的差异度，若差异度高，直接分割，将待合并列表中的单元拼接成一个块，加入最终结果。若差异度低，会临时突破 chunk_size，把这个相关单元也加入，再分割。 import os os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" from langchain_classic.text_splitter import SentenceTransformersTokenTextSplitter from langchain_community.document_loaders import TextLoader semantic_splitter = SentenceTransformersTokenTextSplitter( chunk_size=20, chunk_overlap=5, model_name="paraphrase-multilingual-MiniLM-L12-v2" # 直接用模型名，从镜像下载 ) loader = TextLoader("test.txt") docs = loader.load() chunks = semantic_splitter.split_text(str(docs)) print("分割结果：") for i, chunk in enumerate(chunks): print(f"Chunk {i+1}: {chunk}") 文档结构分块：langchain_classic提供了针对多项结构清晰文档的分块器，如HTMLHeaderTextSplitter、MarkdownHeaderTextSplitter等，如MarkDown大致原理如下： 定义标题层级规则：提供一个标题层级的映射关系，告诉分块器那些是几级标题 提取内容：分割器逐行扫描 Markdown 文本，通过正则/字符串匹配识别所有符合规则的标题行，同时记录标题的层级、在文档中的位置 生成文本块：每个块包含元数据（记录当前块的完整标题层级）以及内容（标题文字 + 其管辖的所有内容） 如果某个标题下的内容超过你设定的 chunk_size，分割器会自动调用 RecursiveCharacterTextSplitter 对内容进行二次拆分，保证块大小符合要求，同时保留标题元数据。 for sep, name in self.headers_to_split_on: is_standard_header = stripped_line.startswith(sep) and ( len(stripped_line) == len(sep) or stripped_line[len(sep)] == " " ) is_custom_header = self._is_custom_header(stripped_line, sep) if is_standard_header or is_custom_header: if name is not None: if sep in self.custom_header_patterns: current_header_level = self.custom_header_patterns[sep] else: current_header_level = sep.count("#") while ( header_stack and header_stack[-1]["level"] >= current_header_level ): popped_header = header_stack.pop() if popped_header["name"] in initial_metadata: initial_metadata.pop(popped_header["name"]) if is_custom_header: header_text = stripped_line[len(sep) : -len(sep)].strip() else: header_text = stripped_line[len(sep) :].strip() header: HeaderType = { "level": current_header_level, "name": name, "data": header_text, } header_stack.append(header) if current_content: lines_with_metadata.append( { "content": "\n".join(current_content), "metadata": current_metadata.copy(), } ) current_content.clear() if not self.strip_headers: current_content.append(stripped_line) break 向量嵌入 1、Embedding（向量嵌入） 将所有的数据对象转换成低维、连续的数值向量技术，相当于赋予一个独一无二的坐标。当语义相似的对象，它对应向量在多维空间中的距离会较近，一般有以下方式度量距离： 余弦相似度 (Cosine Similarity) ：计算两个向量夹角的余弦值。值越接近 1，代表方向越一致，语义越相似。这是最常用的度量方式。 点积 (Dot Product) ：计算两个向量的乘积和。在向量归一化后，点积等价于余弦相似度。 欧氏距离 (Euclidean Distance) ：计算两个向量在空间中的直线距离。距离越小，语义越相似 2、Transformer自注意力机制的诞生，使BERT 模型通过堆叠多个 Transformer 的编码器提出了预训练语言模型，实现了真正深度双向上下文理解。 BERT 的输入由三部分组成，通过嵌入层 (Embedding) 融合： Token Embeddings：词 / 子词的基础向量表示（使用 WordPiece 分词） Segment Embeddings：区分两个句子（如问答任务中的问题和上下文） Position Embeddings：编码词在句子中的位置信息 特殊标记： [CLS]：放在句子开头，用于分类任务的聚合表示 [SEP]：分隔两个句子或标记句子结束 [MASK]：用于预训练的掩码语言模型任务 3、预训练模型主要的训练任务： 掩码语言模型 (MLM) 随机掩盖 15% 的输入词（用 [MASK] 标记） 训练模型预测被掩盖的原始词 确保模型学习上下文依赖性，而非简单记忆 下一句预测 (NSP) 输入一对句子 (A,B) 50% 概率 B 是 A 的真实下一句，50% 概率是随机句子 训练模型判断 B 是否为 A 的下一句 帮助模型学习句子间的逻辑关系和连贯性 现代模型还会引入度量学习和三元组对比学习进行针对性的训练： 度量学习 对比学习 文本对 三元组：(Anchor, Positive, Negative) 正例对拉近、负例对拉远 Anchor 和 Positive 是相关的，Anchor 和 Negative 是不相关的 全局的相对距离分布 局部的两两对比关系 较宽松：只保证正例整体比负例近 更严格：同一锚点下，正例必须显著比负 简单匹配、粗粒度相似度排序精细检索 聚类、小样本分类、句向量编码 大语言模型的综合性能：https://huggingface.co/spaces/mteb/leaderboard 多模态嵌入 多模态嵌入：由于现实世界有图像、音频、视频等各种数据信息，向量嵌入仅能满足文本，多模态嵌入将不同类型的数据（如图像和文本）映射到同一个共享的向量空间。 如OpenAI 的 CLIP (Contrastive Language-Image Pre-training)核心结构： 图像编码器（如 ResNet、Vision Transformer，ViT）：只负责把图片编码成一个固定维度的向量 I 文本编码器（如 Transformer、类 BERT 结构）：只负责把文本编码成一个固定维度的向量 T 对齐语义：训练目标不是让图像和文本特征长得一样，而是语义等价的图文，在共享向量空间里挨得近；语义无关的，离得远 对比学习：正例拉近，负例推远，只是 CLIP 把它用在了「图像 - 文本」两种模态之间。 import os os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com' import torch from PIL import Image import numpy as np from transformers import CLIPProcessor, CLIPModel, CLIPConfig device = "cuda" if torch.cuda.is_available() else "cpu" model_path = "./models/damo/multi-modal_clip-vit-base-patch16_zh" model_name = "openai/clip-vit-base-patch32" model = CLIPModel.from_pretrained(model_name).to(device) processor = CLIPProcessor.from_pretrained(model_name) def compute_similarity(image_features, text_features): image_features = image_features / image_features.norm(dim=-1, keepdim=True) text_features = text_features / text_features.norm(dim=-1, keepdim=True) similarity = (image_features @ text_features.T).squeeze() return similarity.cpu().numpy() image = Image.open("./img.png").convert("RGB") test_texts = [ "a photo of a cat", "一只可爱的小猫", "a photo of a dog", "a red car", "a black and white cat", "an animal with fur", "a person riding a bicycle", "a cat sitting on a couch", "orange cat" ] print("\n正在处理图像和文本...") inputs = processor(text=test_texts, images=image, return_tensors="pt", padding=True).to(device) with torch.no_grad(): outputs = model(**inputs) image_features = outputs.image_embeds text_features = outputs.text_embeds similarities = compute_similarity(image_features, text_features) sorted_indices = np.argsort(similarities)[::-1] for rank, idx in enumerate(sorted_indices, 1): similarity_score = similarities[idx] text = test_texts[idx] if similarity_score > 0.25: marker = "✓" color_code = "\033[92m" elif similarity_score > 0.15: marker = "~" color_code = "\033[93m" else: marker = "✗" color_code = "\033[91m" print(f"{rank:2d}. {marker} {color_code}相似度: {similarity_score:.4f}\033[0m") print(f" 文本: {text}") if rank < len(sorted_indices): print() print("\n" + "=" * 70) print(f"最高相似度: {similarities[sorted_indices[0]]:.4f}") print(f"最低相似度: {similarities[sorted_indices[-1]]:.4f}") print(f"平均相似度: {np.mean(similarities):.4f}") 向量数据库 维度 向量数据库 传统数据库 (RDBMS) 核心数据类型 高维向量 (Embeddings) 结构化数据 (文本、数字、日期) 查询方式 相似性搜索 (ANN) 精确匹配 索引机制 HNSW, IVF, LSH 等 ANN 索引 B-Tree, Hash Index 数据规模 轻松应对千亿级向量 通常在千万到亿级行数据，更大规模需复杂分库分表 性能特点 高维数据检索性能极高，计算密集型 结构化数据查询快，高维数据查询性能呈指数级下降 一致性 通常为最终一致性 强一致性 (ACID 事务) 向量数据库索引机制： 1、LSH（Locality Sensitive Hashing，局部敏感哈希）：让「相似的向量更大概率映射到同一个哈希桶」，检索时只需在同一个桶内计算相似度，避免全量遍历。 用多个独立的哈希函数对向量进行哈希，每个哈希函数都满足 “相似向量哈希冲突概率更高” 的局部敏感特性。 把向量分配到对应的哈希桶中，检索时先通过哈希函数找到查询向量所在的桶，再在桶内做精确相似度计算。 为了提高召回率，通常会用多个哈希函数生成多个哈希表，最终取所有哈希表结果的交集 / 并集 2、IVF（Inverted File，倒排文件）：先把向量空间聚类成多个 “中心”，再把向量分配到最近的中心对应的桶里。检索时先定位到查询向量的最近几个中心，只在这些中心的桶内做精确搜索 聚类建桶：用 K-means 等算法把所有向量聚成 nlist 个簇（中心），每个簇对应一个桶。 向量分配：把每个向量分配到距离最近的中心所在的桶。 检索流程：先计算查询向量与所有中心的相似度，找到最近的 k 个中心，然后只在这 k 个中心的桶内计算向量相似度，得到最终结果。 3、HNSW（Hierarchical Navigable Small World，分层可导航小世界图）：构建多层图结构，检索时先在上层快速定位到大致区域，再逐层下探，最终找到最近邻。 多层图结构：底层是包含所有向量的稠密图，上层是底层图的稀疏采样。每一层的节点都是下层节点的子集，且边的数量更少。 小世界网络特性：图中任意两个节点之间只需通过少量边即可到达，保证了检索的高效性。 检索流程：从最上层开始，通过贪心算法找到距离查询向量最近的节点，然后逐层向下，在每层中优化候选节点，最终在底层得到精确的最近邻。 当前主流的向量数据库 产品名称 主要类型/模式 核心特点 典型适用场景 Pinecone 全托管云服务 开箱即用、自动扩缩容、零运维负担 快速原型验证、实时推荐系统、追求开发效率的场景 Milvus 开源，可自托管或云托管 专为海量向量设计、分布式架构、性能强悍、生态丰富 超大规模图像/视频检索、大规模推荐系统 Weaviate 开源/托管可选 原生支持混合搜索（向量+关键词）、模块化、GraphQL接口 知识图谱、复杂混合搜索、快速构建AI应用 Qdrant 开源，可自托管或云托管 用Rust编写、性能与资源效率高、支持丰富的数据过滤 实时推荐、广告系统、对性能有高要求的场景 Chroma 轻量级嵌入式/开源 极其简单、API友好、与LLM应用（如RAG）集成紧密 本地开发测试、快速原型、中小型RAG应用 腾讯云向量数据库 (VDB) 全托管云服务 开箱即用、深度集成腾讯云生态及AI套件、宣称高性价比 大模型知识库、混合检索等腾讯云生态内场景 Vespa.ai 开源，可自托管 支持复杂排序和过滤的低延迟计算平台，在GigaOm 2025年报告中被评为领导者 需要低延迟、复杂自定义处理的大规模数据计算 FAISS 开源 提供多种高效的近似最近邻搜索算法，追求极致的搜索速度和资源效率 作为高性能搜索引擎，或用于对延迟极度敏感、数据规模固定的研究与原型开发。 在本地可以通过FAISS+Langchain构建一个强大的本地向量存储方案，非常适合快速原型设计和中小型应用 import os import torch os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com' os.environ['HF_HOME'] = './models/huggingface' from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS from langchain_classic.text_splitter import RecursiveCharacterTextSplitter from langchain_core.documents import Document device = "cuda" if torch.cuda.is_available() else "cpu" print("正在加载嵌入模型...") embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-small-zh-v1.5", model_kwargs={'device': device} ) documents = [ Document(page_content="Python是一种高级编程语言，由Guido van Rossum于1991年首次发布。它具有简洁明了的语法，支持多种编程范式，包括面向对象、命令式、函数式和过程式编程。"), Document(page_content="LangChain是一个用于开发由语言模型驱动的应用程序的框架。它提供了模块化的组件，可以轻松地构建复杂的AI应用，包括聊天机器人、问答系统等。"), Document(page_content="FAISS（Facebook AI Similarity Search）是Meta开发的一个用于高效相似性搜索和密集向量聚类的库。它特别适合处理大规模向量数据，支持GPU加速。"), Document(page_content="向量数据库是一种专门用于存储和检索向量数据的数据库。它们通过计算向量之间的相似度来查找最相关的数据，广泛应用于推荐系统、图像检索和语义搜索等领域。"), Document(page_content="RAG（Retrieval-Augmented Generation）是一种结合检索和生成的AI技术。它先从知识库中检索相关信息，然后基于这些信息生成答案，可以提高回答的准确性和可靠性。"), Document(page_content="Transformer是一种基于自注意力机制的深度学习模型架构，由Google在2017年提出。它是现代大语言模型（如GPT、BERT）的基础，能够有效地处理序列数据。"), Document(page_content="微调是指在预训练模型的基础上，使用特定领域的数据进行进一步训练，使模型适应特定任务的过程。相比于从头训练，微调需要的数据量和计算资源都更少。"), Document(page_content="提示词工程是指设计和优化输入给语言模型的提示词，以获得更好的输出结果的技术。好的提示词可以显著提高模型的性能和准确性。"), ] text_splitter = RecursiveCharacterTextSplitter( chunk_size=200, chunk_overlap=50, length_function=len, ) print("\n正在分割文档...") texts = text_splitter.split_documents(documents) print(f"分割后得到 {len(texts)} 个文本块") print("\n正在创建FAISS向量数据库...") vectorstore = FAISS.from_documents(texts, embeddings) print("向量数据库创建完成！") print("\n" + "=" * 70) print("相似度搜索测试") print("=" * 70) queries = [ "什么是向量数据库？", "Python有什么特点？", "如何提高AI模型的准确性？", "Transformer是什么？", ] for query in queries: print(f"\n查询: {query}") print("-" * 70) results = vectorstore.similarity_search_with_score(query, k=3) for i, (doc, score) in enumerate(results, 1): print(f"\n结果 {i} (相似度: {score:.4f}):") print(f"内容: {doc.page_content}") print("=" * 70) print("保存向量数据库") print("=" * 70) vectorstore.save_local("./faiss_index") print("向量数据库已保存到 ./faiss_index 目录") print("\n" + "=" * 70) print("加载向量数据库并测试") print("=" * 70) loaded_vectorstore = FAISS.load_local("./faiss_index", embeddings, allow_dangerous_deserialization=True) print("向量数据库加载成功！") test_query = "什么是RAG技术？" print(f"\n测试查询: {test_query}") print("-" * 70) results = loaded_vectorstore.similarity_search_with_score(test_query, k=2) for i, (doc, score) in enumerate(results, 1): print(f"\n结果 {i} (相似度: {score:.4f}):") print(f"内容: {doc.page_content}") print("\n" + "=" * 70) print("测试完成！") print("=" * 70) Milvus 环境安装 wget https://github.com/milvus-io/milvus/releases/download/v2.5.14/milvus-standalone-docker-compose.yml -O docker-compose.yml 样例： import torch from PIL import Image import numpy as np from typing import List, Dict, Union, Optional from pymilvus import ( connections, utility, FieldSchema, CollectionSchema, DataType, Collection, ) from transformers import CLIPProcessor, CLIPModel import os os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com' os.environ['HF_HOME'] = './models/huggingface' class MultiModalRetriever: def __init__( self, model_name: str = "openai/clip-vit-base-patch32", milvus_host: str = "192.168.78.128", milvus_port: int = 19530, collection_name: str = "multimodal_search", device: Optional[str] = None, recreate_collection: bool = False, ): self.device = device if device else ("cuda" if torch.cuda.is_available() else "cpu") self.model_name = model_name self.milvus_host = milvus_host self.milvus_port = milvus_port self.collection_name = collection_name self.collection = None self.recreate_collection = recreate_collection print(f"使用设备: {self.device}") self._load_model() self._connect_milvus() self._create_collection() def _load_model(self): print(f"正在加载模型: {self.model_name}") try: self.model = CLIPModel.from_pretrained(self.model_name).to(self.device) self.model.eval() self.processor = CLIPProcessor.from_pretrained(self.model_name) print("模型加载成功！") except Exception as e: print(f"模型加载失败: {e}") raise def _connect_milvus(self): print(f"正在连接 Milvus: {self.milvus_host}:{self.milvus_port}") try: connections.connect( alias="default", host=self.milvus_host, port=self.milvus_port ) print("Milvus 连接成功！") except Exception as e: print(f"Milvus 连接失败: {e}") raise def _create_collection(self): if utility.has_collection(self.collection_name): if self.recreate_collection: print(f"集合 {self.collection_name} 已存在，正在删除并重新创建...") utility.drop_collection(self.collection_name) else: print(f"集合 {self.collection_name} 已存在，正在加载...") self.collection = Collection(self.collection_name) self.collection.load() print("集合加载成功！") return print(f"正在创建集合: {self.collection_name}") fields = [ FieldSchema(name="id", dtype=DataType.VARCHAR, is_primary=True, max_length=100), FieldSchema(name="type", dtype=DataType.VARCHAR, max_length=10), FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=65535), FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=512), ] schema = CollectionSchema( fields=fields, description="多模态检索集合" ) self.collection = Collection( name=self.collection_name, schema=schema ) index_params = { "index_type": "IVF_FLAT", "metric_type": "IP", "params": {"nlist": 128} } self.collection.create_index( field_name="embedding", index_params=index_params ) self.collection.load() print("集合创建并加载成功！") def encode_text(self, texts: List[str]) -> np.ndarray: with torch.no_grad(): inputs = self.processor( text=texts, return_tensors="pt", padding=True, truncation=True ).to(self.device) text_features = self.model.get_text_features(**inputs) text_features = text_features / text_features.norm(dim=-1, keepdim=True) embeddings = text_features.cpu().numpy() return embeddings def encode_image(self, images: List[Union[str, Image.Image]]) -> np.ndarray: processed_images = [] for img in images: if isinstance(img, str): img = Image.open(img).convert("RGB") processed_images.append(img) with torch.no_grad(): inputs = self.processor( images=processed_images, return_tensors="pt" ).to(self.device) image_features = self.model.get_image_features(**inputs) image_features = image_features / image_features.norm(dim=-1, keepdim=True) embeddings = image_features.cpu().numpy() return embeddings def insert_texts( self, texts: List[str], ids: Optional[List[str]] = None ) -> int: if ids is None: ids = [f"text_{i}" for i in range(len(texts))] embeddings = self.encode_text(texts) data = [ ids, ["text"] * len(texts), texts, embeddings.tolist() ] insert_result = self.collection.insert(data) self.collection.flush() print(f"成功插入 {len(texts)} 条文本数据") return insert_result.insert_count def insert_images( self, image_paths: List[str], ids: Optional[List[str]] = None, descriptions: Optional[List[str]] = None ) -> int: if ids is None: ids = [f"image_{i}" for i in range(len(image_paths))] if descriptions is None: descriptions = image_paths embeddings = self.encode_image(image_paths) data = [ ids, ["image"] * len(image_paths), descriptions, embeddings.tolist() ] insert_result = self.collection.insert(data) self.collection.flush() print(f"成功插入 {len(image_paths)} 条图像数据") return insert_result.insert_count def search( self, query: Union[str, List[str], Image.Image, List[Image.Image]], top_k: int = 10, search_type: Optional[str] = None ) -> List[Dict]: if isinstance(query, str): query_embeddings = self.encode_text([query]) query_type = "text" elif isinstance(query, Image.Image): query_embeddings = self.encode_image([query]) query_type = "image" elif isinstance(query, list) and len(query) > 0: if isinstance(query[0], str): query_embeddings = self.encode_text(query) query_type = "text" else: query_embeddings = self.encode_image(query) query_type = "image" else: raise ValueError("不支持的查询类型") search_params = { "metric_type": "IP", "params": {"nprobe": 10} } results = self.collection.search( data=query_embeddings.tolist(), anns_field="embedding", param=search_params, limit=top_k, expr=None if search_type is None else f"type == '{search_type}'", output_fields=["id", "type", "content"] ) formatted_results = [] for idx, result in enumerate(results[0]): result_dict = { "id": result.id, "score": result.score } if hasattr(result, "entity"): result_dict["type"] = result.entity.get("type") result_dict["content"] = result.entity.get("content") elif hasattr(result, "get"): result_dict["type"] = result.get("type") result_dict["content"] = result.get("content") else: result_dict["type"] = None result_dict["content"] = None formatted_results.append(result_dict) return formatted_results def hybrid_search( self, text_query: str, image_query: Union[str, Image.Image], top_k: int = 10, text_weight: float = 0.5, image_weight: float = 0.5 ) -> List[Dict]: text_results = self.search(text_query, top_k=top_k) image_results = self.search(image_query, top_k=top_k) combined_scores = {} for result in text_results: if result["id"] not in combined_scores: combined_scores[result["id"]] = { "id": result["id"], "type": result["type"], "content": result["content"], "score": result['score'] } combined_scores[result["id"]]["score"] += result["score"] * text_weight for result in image_results: if result["id"] not in combined_scores: combined_scores[result["id"]] = { "id": result["id"], "type": result["type"], "content": result["content"], "score": result['score'] } combined_scores[result["id"]]["score"] += result["score"] * image_weight sorted_results = sorted( combined_scores.values(), key=lambda x: x["score"], reverse=True ) return sorted_results[:top_k] def delete_collection(self): if utility.has_collection(self.collection_name): utility.drop_collection(self.collection_name) print(f"集合 {self.collection_name} 已删除") def close(self): connections.disconnect("default") print("Milvus 连接已关闭") def main(): print("=" * 70) print("多模态检索引擎示例") print("=" * 70) retriever = MultiModalRetriever( model_name="openai/clip-vit-base-patch32", milvus_host="192.168.78.128", milvus_port=19530, collection_name="multimodal_search", recreate_collection=True ) print("\n" + "=" * 70) print("插入示例文本数据") print("=" * 70) sample_texts = [ "一只可爱的橘色小猫坐在沙发上", "红色跑车在高速公路上飞驰", "一个人骑着自行车穿过公园", "蓝天白云下的美丽风景", "现代科技感的城市建筑", "美味的披萨放在木桌上", "森林中的小溪流水潺潺", "夜晚的星空璀璨夺目", ] image_paths = [ "./images/cat.png", "./images/dog.png", "./images/红色跑车.png", ] retriever.insert_texts(sample_texts) retriever.insert_images(image_paths) print("\n" + "=" * 70) print("文本检索测试") print("=" * 70) query_text = "猫" print(f"\n查询: {query_text}") print("-" * 70) results = retriever.search(query_text, top_k=3) for i, result in enumerate(results, 1): print(f"\n结果 {i} (相似度: {result['score']:.4f}):") print(f"类型: {result['type']}") print(f"内容: {result['content']}") retriever.insert_images(image_paths) text_query = "橘色的猫" image_query = Image.open("./images/img.png") print(f"\n文本查询: {text_query}") print(f"图像查询: img.png") results = retriever.hybrid_search( text_query=text_query, image_query=image_query, top_k=3, text_weight=0, image_weight=1 ) for i, result in enumerate(results, 1): print(f"{i}. [{result['type']}] 综合相似度: {result['score']:.4f}") print(f" {result['content']}\n") retriever.close() if __name__ == "__main__": main() 检索 稀疏向量：向量的长度通常很高，但绝大多数元素的值都是 0，只有极少数位置是非零值，非零值的占比通常远低于 5%，使它的存储非常高效，但是无法捕捉语义关联，如BM25算法： $$ BM25(D,Q) = Σ [ IDF(qi) × ((k1+1)×tf(qi,D))/(k1×((1−b)+b×|D|/avgdl) + tf(qi,D)) ] $$ 密集向量：嵌入向量（Embedding Vector），向量的长度（维度）通常较低几十、几百、几千维），且绝大多数（或全部）元素都是非零值，没有明显的稀疏性，能够捕抓语义关联。 比如一个8维数组，番茄在第3位、炒在第5位，蛋在第7位，权重分别是1.2、0.7、0.8 稀疏向量的坐标列表可表示为： (8, [3, 5, 7], [1.2, 0.7, 0.8]) 密集向量则可能为，密集向量更有可能匹配到炒蛋类的菜： [0.89, -0.12, 0.77, ..., -0.45] 在现代的RAG中，很少单独使用 BM25（稀疏向量）或单独使用密集向量，而是采用「混合检索」： 先用 BM25 进行稀疏检索，召回和查询词「字面匹配」的相关文档，保证召回的准确性和可解释性。 再用密集向量进行语义检索，召回和查询「语义匹配」的相关文档，弥补 BM25 无法捕捉语义的短板（比如查询「苹果的电子产品」，BM25 可能只召回含「苹果」和「电子产品」的文档，而密集向量能召回含「iPhone」「Mac」的文档）。 最后对两种检索结果进行融合排序，得到更全面、更精准的最终结果。 如RFF算法：将多个独立排序结果（如不同检索模型、不同查询方式、不同数据源）合并为单一、更优的排序列表，完全基于排名位置而非原始分数，彻底避开分数可比性问题。 对于文档d，其RFFF总分为所有参与融合的检索系统中的得分加权和： $$ RRF(d) = Σ [ wᵢ / (k + rankᵢ(d)) ] $$ wᵢ：第 i 个检索系统的权重（默认 = 1，平等权重） k：平滑参数（经验值默认 = 60，控制排名衰减速度） rankᵢ(d)：文档 d 在第 i 个检索系统中的排名（通常 1-indexed，未出现则视为无穷大） 现实世界中，有许多数据都是非结构化数据，要从非结构化的数据中检索信息，需要利用大模型的理解能力，将非结构化的数据翻译成针对特定数据源的结构化数据查询语言或带有过滤条件的请求，这个过程叫查询构建。 1、文本到元数据过滤器：构建向量索引时，常常会为文档块附加元数据，这些元数据能辅佐做语义搜索之外的精确过滤功能。如langchain中的SelfQueryRetriever自查询检索器。 接收输入：获取用户包含「语义需求」和「元数据过滤需求」的原始自然语言查询。 LLM 解析：查询构造器调用LLM，结合预定义的元数据 Schema 和文档描述，生成两个结果 ——① 优化后的纯语义查询（保留核心语义，去除过滤条件）；② 通用抽象结构化过滤条件（与向量库无关）。 格式翻译：结构化查询翻译器将抽象过滤条件，转为当前向量库（如 Chroma）支持的原生过滤语法。 双重检索：向量库先执行语义检索（匹配纯语义查询），再对结果做元数据精准过滤（匹配翻译后条件），得到双重符合要求的文档。 返回结果：将文档按相似度排序，格式化（含内容 + 元数据）后返回给用户。 from dotenv import load_dotenv import os from langchain_chroma import Chroma from langchain_classic.chains.query_constructor.base import AttributeInfo from langchain_classic.retrievers.self_query.base import SelfQueryRetriever from langchain_community.chat_models import ChatTongyi from langchain_core.documents import Document from langchain_community.embeddings import DashScopeEmbeddings from langchain_core.documents import Document load_dotenv() dashscope_api_key="sk-XXX" llm = ChatTongyi( model="qwen-turbo", api_key=dashscope_api_key ) embeddings = DashScopeEmbeddings(dashscope_api_key=dashscope_api_key) documents = [ Document( page_content="大语言模型（LLM）的注意力机制详解，包括自注意力、交叉注意力的实现原理和优化技巧。", metadata={ "doc_type": "技术文档", "publish_year": 2024, "author": "李四", "domain": "人工智能" } ), Document( page_content="Chroma向量数据库的本地部署与API使用指南，包含数据入库、检索、过滤的完整示例。", metadata={ "doc_type": "操作手册", "publish_year": 2023, "author": "张三", "domain": "大数据" } ), Document( page_content="RAG系统搭建最佳实践，涵盖混合检索（BM25+向量检索）、RRF融合、精排优化。", metadata={ "doc_type": "技术文档", "publish_year": 2024, "author": "张三", "domain": "人工智能" } ), Document( page_content="网络安全威胁检测方法，包括基于特征匹配和基于异常行为的两种核心方案。", metadata={ "doc_type": "安全报告", "publish_year": 2022, "author": "王五", "domain": "网络安全" } ), Document( page_content="Sentence-BERT嵌入模型的微调教程，针对中文场景优化分词和语义表征能力。", metadata={ "doc_type": "技术文档", "publish_year": 2024, "author": "李四", "domain": "自然语言处理" } ) ] vector_db = Chroma.from_documents( documents=documents, embedding=embeddings, persist_directory="./chroma_self_query_demo" # 本地存储路径，可查看生成的文件 ) metadata_field_info = [ AttributeInfo( name="doc_type", description="文档的类型，可选值：技术文档、操作手册、安全报告", type="string" ), AttributeInfo( name="publish_year", description="文档的发布年份，是一个整数", type="integer" ), AttributeInfo( name="author", description="文档的作者姓名，是一个字符串", type="string" ), AttributeInfo( name="domain", description="文档所属的技术领域，可选值：人工智能、大数据、网络安全、自然语言处理", type="string" ) ] document_content_description = "各类技术领域的文档，涵盖人工智能、大数据、网络安全等方向" self_query_retriever = SelfQueryRetriever.from_llm( llm=llm, vectorstore=vector_db, document_contents=document_content_description, metadata_field_info=metadata_field_info, verbose=True, # 开启详细日志，可查看LLM解析出的结构化查询条件 k=1 # 默认返回前3条匹配结果 ) query="查询2024年发布的文档，并且是张三发布的文档" result=self_query_retriever.invoke(query) for index,message in enumerate(result,1): print(message.page_content) print(message.metadata) 配置元数据字段 (metadata_field_info) ：通过 AttributeInfo 为每个元数据字段定义名称、类型和一份清晰的自然语言 description，LLM依赖元数据理解和处理用户查询 创建自查询检索器 (SelfQueryRetriever.from_llm) ：from_llm 方法在底层执行了两个核心操作： 加载查询构造器：利用方法load_query_constructor_runnable创建一个可查询的构造器，将用户的自然语言查询转换为一个通用的、结构化的查询对象。 获取内置翻译器：通过往_get_builtin_translator方法中传入向量数据，自动匹配一个内置的翻译器。将上一步生成的通用查询对象，翻译成 Chroma 数据库能够原生理解和执行的过滤语法。 def from_llm( cls, llm: BaseLanguageModel, vectorstore: VectorStore, document_contents: str, metadata_field_info: Sequence[AttributeInfo | dict], structured_query_translator: Visitor | None = None, chain_kwargs: dict | None = None, enable_limit: bool = False, # noqa: FBT001,FBT002 use_original_query: bool = False, # noqa: FBT001,FBT002 **kwargs: Any, ) -> "SelfQueryRetriever": if structured_query_translator is None: structured_query_translator = _get_builtin_translator(vectorstore) chain_kwargs = chain_kwargs or {} #提取「当前向量库支持的默认运算符列表」，自动补全到chain_kwargs中 if ( "allowed_comparators" not in chain_kwargs and structured_query_translator.allowed_comparators is not None ): chain_kwargs["allowed_comparators"] = ( structured_query_translator.allowed_comparators ) if ( "allowed_operators" not in chain_kwargs and structured_query_translator.allowed_operators is not None ): chain_kwargs["allowed_operators"] = ( structured_query_translator.allowed_operators ) query_constructor = load_query_constructor_runnable( llm, document_contents, metadata_field_info, enable_limit=enable_limit, **chain_kwargs, ) query_constructor = query_constructor.with_config( run_name=QUERY_CONSTRUCTOR_RUN_NAME, ) return cls( query_constructor=query_constructor, vectorstore=vectorstore, use_original_query=use_original_query, structured_query_translator=structured_query_translator, **kwargs, ) 除扁平化元数据，还支持Cypher图数据库，通过利用大语言模型将用户的自然语言问题直接翻译成一句精准的 Cypher 查询语句 from dotenv import load_dotenv import os from neo4j import GraphDatabase from langchain_community.chat_models import ChatTongyi from langchain_community.graphs import Neo4jGraph from langchain_neo4j import GraphCypherQAChain load_dotenv() dashscope_api_key = os.getenv("DASHSCOPE_API_KEY") neo4j_uri = os.getenv("NEO4J_URI") neo4j_username = os.getenv("NEO4J_USERNAME") neo4j_password = os.getenv("NEO4J_PASSWORD") llm = ChatTongyi( model="qwen-turbo", api_key=dashscope_api_key ) def init_cyber_graph(): # 连接Neo4j数据库 driver = GraphDatabase.driver(neo4j_uri, auth=(neo4j_username, neo4j_password)) # 定义Cypher语句：创建Cyber安全图谱（资产-漏洞-威胁） cypher_commands = [ # 清空现有数据（测试用，避免重复创建） "MATCH (n) DETACH DELETE n", # 创建「资产」节点 """ CREATE (web:Asset {name: "Web服务器", type: "应用服务器", ip: "192.168.1.100", status: "运行中"}) CREATE (db:Asset {name: "MySQL数据库", type: "数据库", ip: "192.168.1.101", status: "运行中"}) CREATE (fw:Asset {name: "防火墙", type: "网络设备", ip: "192.168.1.1", status: "运行中"}) """, # 创建「漏洞」节点 """ CREATE (log4j:Vulnerability {name: "Log4j2远程代码执行", cve: "CVE-2021-44228", severity: "高危", publish_date: "2021-12-10"}) CREATE (sql:Vulnerability {name: "SQL注入", cve: "CVE-2023-XXXX", severity: "中危", publish_date: "2023-05-20"}) CREATE (auth:Vulnerability {name: "弱口令认证", cve: "CVE-2022-YYYY", severity: "高危", publish_date: "2022-08-15"}) """, # 创建「威胁」节点 """ CREATE (ransom:Threat {name: "勒索软件", type: "恶意软件", damage: "严重", target: "企业核心资产"}) CREATE (phish:Threat {name: "钓鱼攻击", type: "社会工程学", damage: "中等", target: "员工账号"}) CREATE (hack:Threat {name: "黑客远程入侵", type: "网络攻击", damage: "极严重", target: "服务器资产"}) """, # 创建节点之间的关联关系 """ # 资产存在漏洞 MATCH (web:Asset {name: "Web服务器"}), (log4j:Vulnerability {name: "Log4j2远程代码执行"}) CREATE (web)-[:HAS_VULNERABILITY {discover_date: "2024-01-05"}]->(log4j) MATCH (db:Asset {name: "MySQL数据库"}), (sql:Vulnerability {name: "SQL注入"}) CREATE (db)-[:HAS_VULNERABILITY {discover_date: "2024-02-10"}]->(sql) MATCH (fw:Asset {name: "防火墙"}), (auth:Vulnerability {name: "弱口令认证"}) CREATE (fw)-[:HAS_VULNERABILITY {discover_date: "2024-03-15"}]->(auth) # 漏洞可被威胁利用 MATCH (log4j:Vulnerability {name: "Log4j2远程代码执行"}), (hack:Threat {name: "黑客远程入侵"}) CREATE (log4j)-[:CAN_BE_EXPLOITED_BY {exploit_success_rate: "80%"}]->(hack) MATCH (sql:Vulnerability {name: "SQL注入"}), (ransom:Threat {name: "勒索软件"}) CREATE (sql)-[:CAN_BE_EXPLOITED_BY {exploit_success_rate: "60%"}]->(ransom) MATCH (auth:Vulnerability {name: "弱口令认证"}), (phish:Threat {name: "钓鱼攻击"}) CREATE (auth)-[:CAN_BE_EXPLOITED_BY {exploit_success_rate: "90%"}]->(phish) """ ] # 执行Cypher语句，构建图谱 with driver.session() as session: for cmd in cypher_commands: session.run(cmd) driver.close() def init_cyber_qa_chain(): # 连接Neo4j图数据库，自动加载图谱结构 graph = Neo4jGraph( url=neo4j_uri, username=neo4j_username, password=neo4j_password ) graph.refresh_schema() print("\n📋 图谱结构详情：") print(graph.get_schema()) qa_chain = GraphCypherQAChain.from_llm( llm=llm, graph=graph, verbose=True, # 开启详细日志，查看生成的Cypher语句 return_direct=False, # False：返回LLM整理后的自然语言答案；True：直接返回Cypher查询结果 cypher_prompt_kwargs={ "schema": graph.get_schema(), # 传入图谱结构，帮助LLM生成准确Cypher "table_context": "Cyber安全图谱，包含资产、漏洞、威胁三类节点及关联关系" } ) return qa_chain if __name__ == "__main__": init_cyber_graph() cyber_qa_chain = init_cyber_qa_chain() print("\n=== 测试查询1：资产漏洞查询 ===") query1 = "Web服务器存在什么漏洞？这个漏洞的CVE编号是什么？" result1 = cyber_qa_chain.invoke({"query": query1}) print(f"📝 答案：{result1['result']}") 查询优化 用户复杂、包含歧义的提问往往和文档存在偏差。用户在查询前有必要进行预处理，包含查询翻译与查询路由两项关键的查询重构。 查询翻译的方法： 提示工程：通过prompt引导 LLM 将用户的原始查询改写得更清晰、更具体，或者对原数据进行一定的排列后再输出。 多查询分解：将一个复杂的问题拆分成多个更简单、更具体的子问题，然后分别对每个子问题进行检索，最后将所有检索到的结果合并、去重，形成一个更全面的上下文。 退步提示：引导 LLM 从用户的原始具体问题中，生成一个更高层次、更概括的退步问题，这个退步问题旨在探寻原始问题背后的通用原理或核心概念。将通用原理作为上下文，再结合原始的具体问题，进行推理并生成最终答案。 假设性嵌入：先利用一个生成式大语言模型（LLM）来生成一个假设性的、能够完美回答该查询的文档，将这个内容详实的假设性文档进行向量化，用其生成的向量去数据库中寻找与之最相似的真实文档。 查询路由： 基于LLM意图识别：当系统具备多种处理能力，需要LLM先进行意图识别，根据意图识别的结果动态选择最合适的检索器或工具。 嵌入相似性路由：为每个路由创建一个详细的文本描述，并使用嵌入模型将其转换为向量。通过map将路由名称和链连起来。后续在发起查询的时候，从模型向量找到相似度最高的路由，再调用路由。 重排 在检索增强生成RAG、信息检索IR流程中，标准链路为：多路召回 → 粗排（初排）→ 重排，重排是RAG系统里用高精度模型对小批量候选做二次精排序的环节，核心是修正召回的粗匹配错误： 1、Cross-Encoder 重排：基于 BERT/RoBERTa 等双塔全交互编码器的重排模型 前置步骤：上游召回 / 粗排产出 Top-K 候选文档（一般 K=50~200，受限于速度）。 输入构造：对每个候选文档 doc，与查询 query拼接为单一序列（通用格式：[CLS] query [SEP] doc [SEP]） 全交互编码：将拼接序列送入 Cross-Encoder，模型内部做 query 与 doc 所有 token 之间的全局自注意力，完成完整语义交互。 相关性打分：取 或对应层的表征，通过线性层映射为 0~1 相关性分数（点 wise 回归 / 二分类）。 重排：所有候选文档按分数降序排列，输出最终结果。 2、ColBERT重排： 离线预处理：用 ColBERT 的文档编码器，将语料中所有文档编码为 词级别嵌入矩阵 $$ Embsdoc∈RLdoc×D（保留每个 token 的细粒度向量，而非单个句向量）。 $$ 在线查询编码：用 ColBERT 的查询编码器，将 query 编码为词级别嵌入矩阵 $$ Embsquery∈RLq×D $$ 延迟交互打分： 计算 query 每个 token 嵌入 与 doc 每个 token 嵌入的余弦相似度； 对 query 每一个 token，取 doc 侧的最大相似度（MaxSim）； 对所有 MaxSim 值求和，得到文档总相关性分数： $$ Score(q,d)=∑t∈qmaxt′∈dcosine(Embq(t),Embd(t′)) $$ 3、RankLLM重排：以通用大语言模型（LLM）为打分 / 排序器的重排方案 上游得到 Top-K 候选文档； 构造面向 LLM 的 Prompt，将 query + 候选文档文本输入 LLM； LLM 输出分数 / 排序关系； 解析结果，对候选集重排。 压缩 这里的压缩指的是对召回 / 重排后的文档文本做内容精简，只保留和 Query 语义相关的片段，剔除无关、冗余、噪声内容，使信息密度提高，LLM聚焦于关键依据。主流的压缩方案一般有： 截断压缩： 取重排后的 Top-N 文档 按规则截断：头部截断、尾部截断、中间滑动窗口取固定长度 字面匹配压缩： 对 Query 做关键词提取（TF-IDF、分词、停用词过滤） 遍历文档，抽取包含最多 Query 关键词的连续句子 / 段落 按顺序拼接片段，作为压缩结果 抽取式语义压缩： 把每篇文档拆成细粒度单元：句子、短 chunk（32~128token） 对每个小块，用模型计算 (Query, 小块) 的相关性分数 按分数筛选 Top-K 小块，按原文顺序拼接 生成式压缩： 输入：Query + 完整文档 Prompt LLM：只保留和Query直接相关的内容，用简洁原文复述，删除无关 LLM 输出一段精简、通顺、只含相关信息的文本 端到端上下文压缩器 以 LangChain ContextualCompressionRetriever实现上下文压缩，使用一个指定的 DocumentCompressor 对这些文档进行处理，封装流程： 基础检索器召回文档 压缩器（可绑定 CrossEncoder/ColBERT/LLM）接收 Query+Docs, 自动做片段打分 + 筛选 + 拼接 直接输出压缩后的文档列表，接入LLM 在langchain中内置了多种DocumentCompressor import os from langchain_community.chat_models import ChatTongyi from langchain_community.embeddings import DashScopeEmbeddings from langchain_core.documents import Document from langchain_community.vectorstores import Chroma from langchain_classic.retrievers import ContextualCompressionRetriever from langchain_classic.retrievers.document_compressors import LLMChainExtractor from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_classic.retrievers.document_compressors import EmbeddingsFilter from langchain_classic.retrievers.document_compressors import LLMChainFilter dashscope_api_key="sk-XXX" llm = ChatTongyi( model="qwen-turbo", api_key=dashscope_api_key ) # 通用嵌入模型（用于FAISS向量库、EmbeddingsFilter） embeddings = DashScopeEmbeddings( model="text-embedding-v1", dashscope_api_key=dashscope_api_key ) # embeddings = HuggingFaceEmbeddings( # model_name="BAAI/bge-small-zh-v1.5", # model_kwargs={'device': device} # ) # 3. 准备测试文档（包含冗余内容，体现压缩效果） documents = [ Document( page_content=""" RAG系统搭建指南（2024版） 【环境准备】（冗余内容） 操作系统：Ubuntu 22.04 Python版本：3.10+ 所需依赖：langchain、chroma、faiss-cpu 【核心步骤】（核心内容） 1. 文档加载与分割：使用RecursiveCharacterTextSplitter分割文档，chunk_size设为500。 2. 向量入库：将分割后的文档通过Embeddings模型向量化，存入FAISS或Chroma。 3. 检索配置：使用混合检索（BM25+向量检索），提升召回率。 4. 结果融合：使用RRF算法融合两种检索结果，避免单一检索的局限性。 【部署注意】（冗余内容） 部署方式：Docker容器化部署 端口配置：映射8080端口对外提供服务 """, metadata={"title": "RAG系统搭建指南", "type": "技术文档"} ), Document( page_content=""" Sentence-BERT微调教程（中文场景） 【硬件要求】（冗余内容） GPU：NVIDIA A10 24G 内存：32G DDR4 【微调步骤】（核心内容） 1. 数据集准备：使用中文语义匹配数据集（如LCQMC），格式为jsonl。 2. 模型加载：加载预训练模型sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2。 3. 训练配置：学习率设为2e-5，训练轮数设为5，batch_size设为32。 4. 效果评估：使用余弦相似度评估微调后模型的语义表征能力，目标相似度≥0.85。 【保存与发布】（冗余内容） 保存格式：pytorch_model.bin 发布平台：Hugging Face Hub """, metadata={"title": "Sentence-BERT微调教程", "type": "技术文档"} ), Document( page_content=""" 办公室网络使用规范（无关文档，用于测试过滤效果） 1. 禁止使用办公网络访问非法网站。 2. 禁止在办公网络中传输涉密文件。 3. 办公设备需定期查杀病毒，每周至少1次。 """, metadata={"title": "办公室网络使用规范", "type": "行政文档"} ) ] faiss_db = Chroma.from_documents(documents, embeddings) base_retriever = faiss_db.as_retriever(search_kwargs={"k": 1}) compressor=LLMChainExtractor.from_llm(llm) compression_retriever=ContextualCompressionRetriever( base_compressor=compressor, base_retriever=base_retriever ) query = "RAG系统搭建的核心步骤有哪些？" base_results=base_retriever.invoke(query) for i, doc in enumerate(base_results, 1): print(f"\n{i}. 文档标题：{doc.metadata['title']}") print(f"内容长度：{len(doc.page_content)} 字符") print(f"内容预览：{doc.page_content}") compressed_results = compression_retriever.invoke(query) for i, doc in enumerate(compressed_results, 1): print(f"\n{i}. 文档标题：{doc.metadata['title']}") print(f"内容长度：{len(doc.page_content)} 字符") print(f"核心内容：\n{doc.page_content}")\ # compressor = EmbeddingsFilter( # embeddings=embeddings, # similarity_threshold=0.5 # ) # # compression_retriever = ContextualCompressionRetriever( # base_compressor=compressor, # base_retriever=base_retriever # ) # # query = "RAG系统搭建的核心步骤有哪些？" # base_results = base_retriever.invoke(query) # for i, doc in enumerate(base_results, 1): # print(f"\n{i}. 文档标题：{doc.metadata['title']}") # print(f"文档类型：{doc.metadata['type']}") # # print("挑选后的文档") # compressed_results = compression_retriever.invoke(query) # for i, doc in enumerate(compressed_results, 1): # print(f"\n{i}. 文档标题：{doc.metadata['title']}") # print(f"文档内容预览：\n{doc.page_content}") # compressor = LLMChainFilter.from_llm(llm) # # compression_retriever = ContextualCompressionRetriever( # base_compressor=compressor, # base_retriever=base_retriever # ) # # query = "RAG系统搭建的核心步骤有哪些？" # print("【基础检索结果（含无关文档）】") # base_results = base_retriever.invoke(query) # for i, doc in enumerate(base_results, 1): # print(f"\n{i}. 文档标题：{doc.metadata['title']}") # print(f"文档类型：{doc.metadata['type']}") # # print("\n【压缩后结果（仅保留相关文档）】") # compressed_results = compression_retriever.invoke(query) # for i, doc in enumerate(compressed_results, 1): # print(f"\n{i}. 文档标题：{doc.metadata['title']}") # print(f"文档完整内容（无片段提取，仅过滤无关文档）：\n{doc.page_content}...") 压缩器类型 核心逻辑 速度 成本 适用场景 LLMChainExtractor 提取文档内相关片段，丢弃冗余 慢 高（LLM 调用，提炼片段） 文档冗余多，需要精准核心片段 LLMChainFilter 判断文档整体相关性，保留 / 丢弃整个文档 中 中（LLM 调用，仅判断） 需快速过滤完全无关文档，核心文档冗余少 EmbeddingsFilter 向量相似度匹配，过滤低于阈值的文档 快 低（无 LLM 调用，仅向量计算） 大规模文档预过滤，降低后续处理压力 参考链接：https://datawhalechina.github.io/all-in-rag/#/chapter2/04_data_load
滑块验证码参数 验证码的 ID：SLIDER_ff970847732c44859fb56935fab7f7c9 鼠标 move 的轨迹，一共记录 36 次 滑块从开始到结束的时间 ImageCaptchaTrack(bgImageWidth=300, bgImageHeight=180, templateImageWidth=null, templateImageHeight=null, startTime=Tue Sep 09 17:30:21 CST 2025, stopTime=Tue Sep 09 17:30:24 CST 2025, left=null, top=null, trackList=[ImageCaptchaTrack.Track(x=0.0, y=0.0, t=1244.0, type=down), ImageCaptchaTrack.Track(x=1.0, y=0.0, t=1268.0, type=move), ImageCaptchaTrack.Track(x=3.0, y=0.0, t=1284.0, type=move), ImageCaptchaTrack.Track(x=10.0, y=0.0, t=1301.0, type=move), ImageCaptchaTrack.Track(x=19.0, y=0.0, t=1318.0, type=move), ImageCaptchaTrack.Track(x=34.0, y=0.0, t=1334.0, type=move), ImageCaptchaTrack.Track(x=46.0, y=0.0, t=1351.0, type=move), ImageCaptchaTrack.Track(x=55.0, y=0.0, t=1368.0, type=move), ImageCaptchaTrack.Track(x=63.0, y=-1.0, t=1384.0, type=move), ImageCaptchaTrack.Track(x=75.0, y=-1.0, t=1401.0, type=move), ImageCaptchaTrack.Track(x=91.0, y=-1.0, t=1418.0, type=move), ImageCaptchaTrack.Track(x=113.0, y=-1.0, t=1435.0, type=move), ImageCaptchaTrack.Track(x=122.0, y=-1.0, t=1451.0, type=move), ImageCaptchaTrack.Track(x=132.0, y=-1.0, t=1468.0, type=move), ImageCaptchaTrack.Track(x=143.0, y=-1.0, t=1485.0, type=move), ImageCaptchaTrack.Track(x=156.0, y=-2.0, t=1501.0, type=move), ImageCaptchaTrack.Track(x=166.0, y=-2.0, t=1518.0, type=move), ImageCaptchaTrack.Track(x=172.0, y=-3.0, t=1535.0, type=move), ImageCaptchaTrack.Track(x=173.0, y=-3.0, t=1551.0, type=move), ImageCaptchaTrack.Track(x=174.0, y=-3.0, t=1568.0, type=move), ImageCaptchaTrack.Track(x=176.0, y=-3.0, t=1585.0, type=move), ImageCaptchaTrack.Track(x=182.0, y=-3.0, t=1602.0, type=move), ImageCaptchaTrack.Track(x=193.0, y=-3.0, t=1618.0, type=move), ImageCaptchaTrack.Track(x=206.0, y=-3.0, t=1635.0, type=move), ImageCaptchaTrack.Track(x=217.0, y=-3.0, t=1652.0, type=move), ImageCaptchaTrack.Track(x=218.0, y=-3.0, t=1668.0, type=move), ImageCaptchaTrack.Track(x=219.0, y=-3.0, t=1685.0, type=move), ImageCaptchaTrack.Track(x=220.0, y=-3.0, t=1701.0, type=move), ImageCaptchaTrack.Track(x=220.0, y=-4.0, t=1718.0, type=move), ImageCaptchaTrack.Track(x=221.0, y=-4.0, t=1735.0, type=move), ImageCaptchaTrack.Track(x=222.0, y=-4.0, t=1751.0, type=move), ImageCaptchaTrack.Track(x=221.0, y=-4.0, t=1835.0, type=move), ImageCaptchaTrack.Track(x=220.0, y=-4.0, t=2002.0, type=move), ImageCaptchaTrack.Track(x=218.0, y=-4.0, t=2018.0, type=move), ImageCaptchaTrack.Track(x=217.0, y=-3.0, t=2035.0, type=move), ImageCaptchaTrack.Track(x=218.0, y=-3.0, t=2369.0, type=move), ImageCaptchaTrack.Track(x=218.0, y=-3.0, t=2659.0, type=up)], data=null) 点选验证码参数 验证码的 ID：SLIDER_ff970847732c44859fb56935fab7f7c9 鼠标 move 的轨迹，一共记录 225 次 滑块从开始到结束的时间 ImageCaptchaTrack(bgImageWidth=300, bgImageHeight=180, templateImageWidth=null, templateImageHeight=null, startTime=Tue Sep 09 17:39:50 CST 2025, stopTime=Tue Sep 09 17:40:04 CST 2025, left=null, top=null, trackList=[ImageCaptchaTrack.Track(x=23.0, y=157.0, t=7061.0, type=click), ImageCaptchaTrack.Track(x=8.0, y=87.0, t=7133.0, type=move), ImageCaptchaTrack.Track(x=14.0, y=83.0, t=7142.0, type=move), ImageCaptchaTrack.Track(x=30.0, y=75.0, t=7159.0, type=move), ImageCaptchaTrack.Track(x=47.0, y=65.0, t=7176.0, type=move), ImageCaptchaTrack.Track(x=63.0, y=55.0, t=7193.0, type=move), ImageCaptchaTrack.Track(x=73.0, y=47.0, t=7209.0, type=move), ImageCaptchaTrack.Track(x=81.0, y=40.0, t=7226.0, type=move), ImageCaptchaTrack.Track(x=87.0, y=35.0, t=7243.0, type=move), ImageCaptchaTrack.Track(x=91.0, y=31.0, t=7259.0, type=move), ImageCaptchaTrack.Track(x=92.0, y=30.0, t=7276.0, type=move), ImageCaptchaTrack.Track(x=93.0, y=30.0, t=7293.0, type=move), ImageCaptchaTrack.Track(x=92.0, y=29.0, t=7454.0, type=move), ImageCaptchaTrack.Track(x=90.0, y=27.0, t=7461.0, type=move), ImageCaptchaTrack.Track(x=88.0, y=25.0, t=7476.0, type=move), ImageCaptchaTrack.Track(x=83.0, y=21.0, t=7493.0, type=move), ImageCaptchaTrack.Track(x=79.0, y=13.0, t=7509.0, type=move), ImageCaptchaTrack.Track(x=76.0, y=8.0, t=7526.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=4.0, t=7543.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=1.0, t=7560.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=-1.0, t=7576.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=-2.0, t=7593.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=-4.0, t=7609.0, type=move), ImageCaptchaTrack.Track(x=78.0, y=-6.0, t=7626.0, type=move), ImageCaptchaTrack.Track(x=84.0, y=-10.0, t=7643.0, type=move), ImageCaptchaTrack.Track(x=91.0, y=-12.0, t=7660.0, type=move), ImageCaptchaTrack.Track(x=97.0, y=-12.0, t=7676.0, type=move), ImageCaptchaTrack.Track(x=106.0, y=-12.0, t=7693.0, type=move), ImageCaptchaTrack.Track(x=122.0, y=-8.0, t=7710.0, type=move), ImageCaptchaTrack.Track(x=129.0, y=-7.0, t=7726.0, type=move), ImageCaptchaTrack.Track(x=134.0, y=-7.0, t=7743.0, type=move), ImageCaptchaTrack.Track(x=137.0, y=-7.0, t=7760.0, type=move), ImageCaptchaTrack.Track(x=139.0, y=-7.0, t=7777.0, type=move), ImageCaptchaTrack.Track(x=141.0, y=-7.0, t=7793.0, type=move), ImageCaptchaTrack.Track(x=143.0, y=-8.0, t=7810.0, type=move), ImageCaptchaTrack.Track(x=144.0, y=-9.0, t=7826.0, type=move), ImageCaptchaTrack.Track(x=145.0, y=-10.0, t=7845.0, type=move), ImageCaptchaTrack.Track(x=145.0, y=-11.0, t=7894.0, type=move), ImageCaptchaTrack.Track(x=145.0, y=-12.0, t=7917.0, type=move), ImageCaptchaTrack.Track(x=146.0, y=-12.0, t=7934.0, type=move), ImageCaptchaTrack.Track(x=147.0, y=-12.0, t=7943.0, type=move), ImageCaptchaTrack.Track(x=154.0, y=-12.0, t=7960.0, type=move), ImageCaptchaTrack.Track(x=161.0, y=-13.0, t=7977.0, type=move), ImageCaptchaTrack.Track(x=166.0, y=-13.0, t=7993.0, type=move), ImageCaptchaTrack.Track(x=167.0, y=-13.0, t=8010.0, type=move), ImageCaptchaTrack.Track(x=168.0, y=-13.0, t=8027.0, type=move), ImageCaptchaTrack.Track(x=169.0, y=-13.0, t=8318.0, type=move), ImageCaptchaTrack.Track(x=170.0, y=-13.0, t=8327.0, type=move), ImageCaptchaTrack.Track(x=171.0, y=-13.0, t=8344.0, type=move), ImageCaptchaTrack.Track(x=172.0, y=-13.0, t=8365.0, type=move), ImageCaptchaTrack.Track(x=178.0, y=-11.0, t=8430.0, type=move), ImageCaptchaTrack.Track(x=182.0, y=-10.0, t=8444.0, type=move), ImageCaptchaTrack.Track(x=195.0, y=-7.0, t=8460.0, type=move), ImageCaptchaTrack.Track(x=210.0, y=-6.0, t=8477.0, type=move), ImageCaptchaTrack.Track(x=214.0, y=-5.0, t=8494.0, type=move), ImageCaptchaTrack.Track(x=214.0, y=-4.0, t=8549.0, type=move), ImageCaptchaTrack.Track(x=212.0, y=-4.0, t=8573.0, type=move), ImageCaptchaTrack.Track(x=208.0, y=-3.0, t=8581.0, type=move), ImageCaptchaTrack.Track(x=205.0, y=-3.0, t=8594.0, type=move), ImageCaptchaTrack.Track(x=198.0, y=-2.0, t=8611.0, type=move), ImageCaptchaTrack.Track(x=193.0, y=-2.0, t=8627.0, type=move), ImageCaptchaTrack.Track(x=190.0, y=-2.0, t=8644.0, type=move), ImageCaptchaTrack.Track(x=188.0, y=-2.0, t=8661.0, type=move), ImageCaptchaTrack.Track(x=186.0, y=-2.0, t=8677.0, type=move), ImageCaptchaTrack.Track(x=185.0, y=-2.0, t=8694.0, type=move), ImageCaptchaTrack.Track(x=192.0, y=-2.0, t=8744.0, type=move), ImageCaptchaTrack.Track(x=206.0, y=-2.0, t=8761.0, type=move), ImageCaptchaTrack.Track(x=221.0, y=-2.0, t=8778.0, type=move), ImageCaptchaTrack.Track(x=231.0, y=-2.0, t=8794.0, type=move), ImageCaptchaTrack.Track(x=236.0, y=-3.0, t=8811.0, type=move), ImageCaptchaTrack.Track(x=237.0, y=-3.0, t=8862.0, type=move), ImageCaptchaTrack.Track(x=238.0, y=-3.0, t=8885.0, type=move), ImageCaptchaTrack.Track(x=239.0, y=-4.0, t=8894.0, type=move), ImageCaptchaTrack.Track(x=240.0, y=-5.0, t=8911.0, type=move), ImageCaptchaTrack.Track(x=242.0, y=-6.0, t=8927.0, type=move), ImageCaptchaTrack.Track(x=243.0, y=-7.0, t=8944.0, type=move), ImageCaptchaTrack.Track(x=245.0, y=-9.0, t=8961.0, type=move), ImageCaptchaTrack.Track(x=246.0, y=-9.0, t=8978.0, type=move), ImageCaptchaTrack.Track(x=248.0, y=-10.0, t=8994.0, type=move), ImageCaptchaTrack.Track(x=251.0, y=-10.0, t=9011.0, type=move), ImageCaptchaTrack.Track(x=256.0, y=-10.0, t=9028.0, type=move), ImageCaptchaTrack.Track(x=260.0, y=-10.0, t=9044.0, type=move), ImageCaptchaTrack.Track(x=264.0, y=-10.0, t=9061.0, type=move), ImageCaptchaTrack.Track(x=265.0, y=-10.0, t=9078.0, type=move), ImageCaptchaTrack.Track(x=264.0, y=-9.0, t=9237.0, type=move), ImageCaptchaTrack.Track(x=279.0, y=60.0, t=9325.0, type=click), ImageCaptchaTrack.Track(x=263.0, y=-8.0, t=9382.0, type=move), ImageCaptchaTrack.Track(x=255.0, y=-6.0, t=9395.0, type=move), ImageCaptchaTrack.Track(x=240.0, y=-5.0, t=9412.0, type=move), ImageCaptchaTrack.Track(x=224.0, y=-5.0, t=9428.0, type=move), ImageCaptchaTrack.Track(x=210.0, y=-5.0, t=9445.0, type=move), ImageCaptchaTrack.Track(x=193.0, y=-5.0, t=9461.0, type=move), ImageCaptchaTrack.Track(x=187.0, y=-6.0, t=9478.0, type=move), ImageCaptchaTrack.Track(x=183.0, y=-7.0, t=9495.0, type=move), ImageCaptchaTrack.Track(x=180.0, y=-8.0, t=9511.0, type=move), ImageCaptchaTrack.Track(x=178.0, y=-8.0, t=9528.0, type=move), ImageCaptchaTrack.Track(x=177.0, y=-8.0, t=9545.0, type=move), ImageCaptchaTrack.Track(x=174.0, y=-9.0, t=9562.0, type=move), ImageCaptchaTrack.Track(x=172.0, y=-9.0, t=9578.0, type=move), ImageCaptchaTrack.Track(x=168.0, y=-10.0, t=9595.0, type=move), ImageCaptchaTrack.Track(x=166.0, y=-10.0, t=9612.0, type=move), ImageCaptchaTrack.Track(x=163.0, y=-12.0, t=9628.0, type=move), ImageCaptchaTrack.Track(x=162.0, y=-12.0, t=9646.0, type=move), ImageCaptchaTrack.Track(x=161.0, y=-12.0, t=9662.0, type=move), ImageCaptchaTrack.Track(x=160.0, y=-12.0, t=9694.0, type=move), ImageCaptchaTrack.Track(x=159.0, y=-12.0, t=9710.0, type=move), ImageCaptchaTrack.Track(x=158.0, y=-12.0, t=9745.0, type=move), ImageCaptchaTrack.Track(x=157.0, y=-12.0, t=9814.0, type=move), ImageCaptchaTrack.Track(x=172.0, y=57.0, t=9893.0, type=click), ImageCaptchaTrack.Track(x=155.0, y=-12.0, t=9974.0, type=move), ImageCaptchaTrack.Track(x=146.0, y=-12.0, t=9981.0, type=move), ImageCaptchaTrack.Track(x=135.0, y=-12.0, t=9995.0, type=move), ImageCaptchaTrack.Track(x=104.0, y=-12.0, t=10012.0, type=move), ImageCaptchaTrack.Track(x=81.0, y=-12.0, t=10029.0, type=move), ImageCaptchaTrack.Track(x=66.0, y=-12.0, t=10045.0, type=move), ImageCaptchaTrack.Track(x=69.0, y=-7.0, t=10126.0, type=move), ImageCaptchaTrack.Track(x=72.0, y=-2.0, t=10133.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=3.0, t=10145.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=15.0, t=10162.0, type=move), ImageCaptchaTrack.Track(x=90.0, y=26.0, t=10179.0, type=move), ImageCaptchaTrack.Track(x=98.0, y=35.0, t=10196.0, type=move), ImageCaptchaTrack.Track(x=107.0, y=44.0, t=10212.0, type=move), ImageCaptchaTrack.Track(x=112.0, y=49.0, t=10229.0, type=move), ImageCaptchaTrack.Track(x=122.0, y=56.0, t=10246.0, type=move), ImageCaptchaTrack.Track(x=129.0, y=61.0, t=10262.0, type=move), ImageCaptchaTrack.Track(x=134.0, y=66.0, t=10279.0, type=move), ImageCaptchaTrack.Track(x=137.0, y=70.0, t=10296.0, type=move), ImageCaptchaTrack.Track(x=138.0, y=71.0, t=10312.0, type=move), ImageCaptchaTrack.Track(x=138.0, y=72.0, t=10329.0, type=move), ImageCaptchaTrack.Track(x=137.0, y=72.0, t=10389.0, type=move), ImageCaptchaTrack.Track(x=135.0, y=72.0, t=10405.0, type=move), ImageCaptchaTrack.Track(x=133.0, y=72.0, t=10422.0, type=move), ImageCaptchaTrack.Track(x=132.0, y=71.0, t=10438.0, type=move), ImageCaptchaTrack.Track(x=131.0, y=70.0, t=10446.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=69.0, t=10462.0, type=move), ImageCaptchaTrack.Track(x=129.0, y=68.0, t=10479.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=68.0, t=10589.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=69.0, t=10629.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=70.0, t=10654.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=71.0, t=10663.0, type=move), ImageCaptchaTrack.Track(x=130.0, y=72.0, t=10765.0, type=move), ImageCaptchaTrack.Track(x=128.0, y=74.0, t=10813.0, type=move), ImageCaptchaTrack.Track(x=122.0, y=74.0, t=10829.0, type=move), ImageCaptchaTrack.Track(x=114.0, y=72.0, t=10846.0, type=move), ImageCaptchaTrack.Track(x=107.0, y=69.0, t=10863.0, type=move), ImageCaptchaTrack.Track(x=101.0, y=64.0, t=10880.0, type=move), ImageCaptchaTrack.Track(x=97.0, y=62.0, t=10896.0, type=move), ImageCaptchaTrack.Track(x=92.0, y=58.0, t=10913.0, type=move), ImageCaptchaTrack.Track(x=89.0, y=55.0, t=10930.0, type=move), ImageCaptchaTrack.Track(x=86.0, y=52.0, t=10946.0, type=move), ImageCaptchaTrack.Track(x=83.0, y=51.0, t=10963.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=50.0, t=10980.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=49.0, t=10998.0, type=move), ImageCaptchaTrack.Track(x=81.0, y=49.0, t=11046.0, type=move), ImageCaptchaTrack.Track(x=79.0, y=48.0, t=11063.0, type=move), ImageCaptchaTrack.Track(x=76.0, y=46.0, t=11080.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=45.0, t=11096.0, type=move), ImageCaptchaTrack.Track(x=71.0, y=43.0, t=11113.0, type=move), ImageCaptchaTrack.Track(x=71.0, y=44.0, t=11190.0, type=move), ImageCaptchaTrack.Track(x=73.0, y=47.0, t=11197.0, type=move), ImageCaptchaTrack.Track(x=77.0, y=52.0, t=11213.0, type=move), ImageCaptchaTrack.Track(x=80.0, y=57.0, t=11230.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=60.0, t=11247.0, type=move), ImageCaptchaTrack.Track(x=82.0, y=62.0, t=11264.0, type=move), ImageCaptchaTrack.Track(x=83.0, y=62.0, t=11293.0, type=move), ImageCaptchaTrack.Track(x=81.0, y=62.0, t=11382.0, type=move), ImageCaptchaTrack.Track(x=78.0, y=61.0, t=11398.0, type=move), ImageCaptchaTrack.Track(x=74.0, y=58.0, t=11413.0, type=move), ImageCaptchaTrack.Track(x=70.0, y=56.0, t=11430.0, type=move), ImageCaptchaTrack.Track(x=69.0, y=55.0, t=11447.0, type=move), ImageCaptchaTrack.Track(x=67.0, y=53.0, t=11464.0, type=move), ImageCaptchaTrack.Track(x=66.0, y=52.0, t=11480.0, type=move), ImageCaptchaTrack.Track(x=64.0, y=52.0, t=11497.0, type=move), ImageCaptchaTrack.Track(x=64.0, y=51.0, t=11514.0, type=move), ImageCaptchaTrack.Track(x=63.0, y=50.0, t=11530.0, type=move), ImageCaptchaTrack.Track(x=62.0, y=48.0, t=11547.0, type=move), ImageCaptchaTrack.Track(x=61.0, y=46.0, t=11564.0, type=move), ImageCaptchaTrack.Track(x=60.0, y=44.0, t=11581.0, type=move), ImageCaptchaTrack.Track(x=58.0, y=43.0, t=11597.0, type=move), ImageCaptchaTrack.Track(x=59.0, y=43.0, t=11846.0, type=move), ImageCaptchaTrack.Track(x=60.0, y=42.0, t=11853.0, type=move), ImageCaptchaTrack.Track(x=61.0, y=42.0, t=11864.0, type=move), ImageCaptchaTrack.Track(x=62.0, y=42.0, t=11881.0, type=move), ImageCaptchaTrack.Track(x=62.0, y=41.0, t=11898.0, type=move), ImageCaptchaTrack.Track(x=63.0, y=41.0, t=11914.0, type=move), ImageCaptchaTrack.Track(x=62.0, y=40.0, t=12485.0, type=move), ImageCaptchaTrack.Track(x=61.0, y=39.0, t=12533.0, type=move), ImageCaptchaTrack.Track(x=67.0, y=41.0, t=13014.0, type=move), ImageCaptchaTrack.Track(x=80.0, y=43.0, t=13022.0, type=move), ImageCaptchaTrack.Track(x=95.0, y=46.0, t=13032.0, type=move), ImageCaptchaTrack.Track(x=133.0, y=52.0, t=13048.0, type=move), ImageCaptchaTrack.Track(x=167.0, y=57.0, t=13065.0, type=move), ImageCaptchaTrack.Track(x=208.0, y=61.0, t=13082.0, type=move), ImageCaptchaTrack.Track(x=240.0, y=63.0, t=13099.0, type=move), ImageCaptchaTrack.Track(x=265.0, y=63.0, t=13115.0, type=move), ImageCaptchaTrack.Track(x=279.0, y=63.0, t=13132.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=62.0, t=13149.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=61.0, t=13166.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=60.0, t=13190.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=59.0, t=13198.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=57.0, t=13215.0, type=move), ImageCaptchaTrack.Track(x=283.0, y=55.0, t=13232.0, type=move), ImageCaptchaTrack.Track(x=281.0, y=50.0, t=13249.0, type=move), ImageCaptchaTrack.Track(x=277.0, y=46.0, t=13266.0, type=move), ImageCaptchaTrack.Track(x=274.0, y=41.0, t=13282.0, type=move), ImageCaptchaTrack.Track(x=267.0, y=37.0, t=13299.0, type=move), ImageCaptchaTrack.Track(x=263.0, y=35.0, t=13315.0, type=move), ImageCaptchaTrack.Track(x=260.0, y=34.0, t=13332.0, type=move), ImageCaptchaTrack.Track(x=259.0, y=34.0, t=13349.0, type=move), ImageCaptchaTrack.Track(x=256.0, y=34.0, t=13366.0, type=move), ImageCaptchaTrack.Track(x=253.0, y=34.0, t=13382.0, type=move), ImageCaptchaTrack.Track(x=249.0, y=35.0, t=13399.0, type=move), ImageCaptchaTrack.Track(x=243.0, y=36.0, t=13416.0, type=move), ImageCaptchaTrack.Track(x=240.0, y=37.0, t=13432.0, type=move), ImageCaptchaTrack.Track(x=237.0, y=37.0, t=13449.0, type=move), ImageCaptchaTrack.Track(x=235.0, y=37.0, t=13465.0, type=move), ImageCaptchaTrack.Track(x=231.0, y=37.0, t=13482.0, type=move), ImageCaptchaTrack.Track(x=227.0, y=37.0, t=13499.0, type=move), ImageCaptchaTrack.Track(x=224.0, y=37.0, t=13516.0, type=move), ImageCaptchaTrack.Track(x=220.0, y=37.0, t=13532.0, type=move), ImageCaptchaTrack.Track(x=217.0, y=37.0, t=13549.0, type=move), ImageCaptchaTrack.Track(x=212.0, y=37.0, t=13566.0, type=move), ImageCaptchaTrack.Track(x=209.0, y=37.0, t=13582.0, type=move), ImageCaptchaTrack.Track(x=207.0, y=37.0, t=13599.0, type=move), ImageCaptchaTrack.Track(x=222.0, y=106.0, t=13758.0, type=click)], data=null) 简单校验 概述： 提供点击式验证码和滑动式验证码的验证机制 通过位置百分比计算和容忍度判断，验证用户操作的准确性 支持存储验证过程中的关键数据（目标值、实际值、偏差等） 点击验证码校验： 提取预设目标点信息（格式为 "x%，y%；x%,y%") 从轨迹中筛选出点击类型（CLICK）的轨迹点 两种验证模式： 有序验证：按顺序匹配点击轨迹与目标点 无序验证：不限制顺序，只要每个目标点都有匹配的点击 通过百分比计算（相对背景图尺寸）和容忍度判断位置是否匹配 滑块验证码校验： 提取预设的目标滑动百分比 计算实际滑动距离（终点 X 坐标 - 起点 X 坐标） 将实际滑动距离转换为相对于背景图宽度的百分比 校验实际百分比与目标百分比是否在容忍范围内 public boolean doValidClickCaptcha(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData, Float tolerant, String type) { String validStr = imageCaptchaValidData.getString(PERCENTAGE_KEY, null); Object checkOrder = imageCaptchaValidData.getOrDefault(CLICK_IMAGE_CHECK_ORDER_KEY, true); if (ObjectUtils.isEmpty(validStr)) { return false; } String[] splitArr = validStr.split(";"); List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList(); if (trackList.size() < splitArr.length) { return false; } // 取出点击事件的轨迹数据 List<ImageCaptchaTrack.Track> clickTrackList = trackList .stream() .filter(t -> TrackTypeConstant.CLICK.equalsIgnoreCase(t.getType())) .collect(Collectors.toList()); if (clickTrackList.size() != splitArr.length) { return false; } StringBuilder sb = new StringBuilder(); List<Double> percentages = new ArrayList<>(); for (int i = 0; i < splitArr.length; i++) { String posStr = splitArr[i]; String[] posArr = posStr.split(","); float xPercentage = Float.parseFloat(posArr[0]); float yPercentage = Float.parseFloat(posArr[1]); float calcXPercentage = 0f; float calcYPercentage = 0f; if (Boolean.TRUE.equals(checkOrder)) { ImageCaptchaTrack.Track track = clickTrackList.get(0); calcXPercentage = calcPercentage(track.getX(), imageCaptchaTrack.getBgImageWidth()); calcYPercentage = calcPercentage(track.getY(), imageCaptchaTrack.getBgImageHeight()); if (!checkPercentage(calcXPercentage, xPercentage, tolerant) || !checkPercentage(calcYPercentage, yPercentage, tolerant)) { return false; } clickTrackList.remove(0); } else { boolean flag = false; for (int a = 0; a < clickTrackList.size(); a++) { ImageCaptchaTrack.Track track = clickTrackList.get(a); calcXPercentage = calcPercentage(track.getX(), imageCaptchaTrack.getBgImageWidth()); calcYPercentage = calcPercentage(track.getY(), imageCaptchaTrack.getBgImageHeight()); if (checkPercentage(calcXPercentage, xPercentage, tolerant) && checkPercentage(calcYPercentage, yPercentage, tolerant)) { // 验证命中 clickTrackList.remove(a); flag = true; break; } } if (!flag) { return false; } } if (i > 0) { sb.append("|"); } sb.append(calcXPercentage).append(",").append(calcYPercentage); percentages.add((double) ((calcXPercentage - xPercentage) + (calcYPercentage - yPercentage))); } // 存储一下当前计算出来的值 return true; } public boolean doValidSliderCaptcha(ImageCaptchaTrack imageCaptchaTrack, AnyMap imageCaptchaValidData, Float tolerant, String type) { Float oriPercentage = imageCaptchaValidData.getFloat(PERCENTAGE_KEY); if (oriPercentage == null) { // 没读取到百分比 return false; } List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList(); ImageCaptchaTrack.Track firstTrack = trackList.get(0); // 取最后一个滑动轨迹 ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1); // 计算百分比 float calcPercentage = calcPercentage(lastTrack.getX() - firstTrack.getX(), imageCaptchaTrack.getBgImageWidth()); // 校验百分比 boolean percentage = checkPercentage(calcPercentage, oriPercentage, tolerant); if (percentage) { // 校验成功 // 存储一下当前计算出来的值 imageCaptchaValidData.put(USER_CURRENT_PERCENTAGE, String.valueOf(calcPercentage)); imageCaptchaValidData.put(USER_CURRENT_PERCENTAGE_STD, String.valueOf(calcPercentage - oriPercentage)); } return percentage; } protected void addPercentage(ImageCaptchaInfo imageCaptchaInfo, AnyMap imageCaptchaValidData) { float percentage = calcPercentage(imageCaptchaInfo.getRandomX(), imageCaptchaInfo.getBackgroundImageWidth()); imageCaptchaValidData.put(PERCENTAGE_KEY, percentage); } } 复杂轨迹校验 // 遍历轨迹列表 Iterator var2 = imageCaptchaTrack.getTrackList().iterator(); do { if (!var2.hasNext()) { return; // 所有轨迹点都校验通过 } ImageCaptchaTrack.Track track = (ImageCaptchaTrack.Track)var2.next(); x = track.getX(); // X坐标 y = track.getY(); // Y坐标 t = track.getT(); // 时间戳 type = track.getType();// 轨迹类型 // 只要有一个轨迹点的任何属性为空，就抛出异常 } while(x != null && y != null && t != null && !ObjectUtils.isEmpty(type)); throw new IllegalArgumentException("track[x,y,t,type] must not be null"); 根据上文获取到的坐标行为轨迹，做的校验： 滑动时间验证：检查滑动开始到结束的时间是否过短（小于 300 毫秒） 轨迹点数量验证：轨迹点数量需满足 10 ≤ 数量 ≤ 背景图宽度×5，太少（<10）可能是简单脚本，太多（> 宽度 ×5）可能是异常数据。 初始位置验证：x 轴和 y 轴应该是从 0 开始的，要是一开始 x 轴和 y 轴乱跑，返回 false 轨迹连续性与合理性验证： 相邻轨迹点的 X 或 Y 方向位移不能超过 50 像素（防止瞬间跳变，真人滑动有速度限制）； 统计 Y 坐标不变的点（机器可能水平匀速滑动，真人难免有上下波动）； 统计 X 坐标超出背景图宽度的点（正常滑动不会越界，过多越界点为异常） 滑动速度变化验证： 验证逻辑：对比滑动过程中前 70% 轨迹与后 30% 轨迹的平均时间间隔。 判断标准：真人滑动通常越到后期速度越快（后 30% 平均时间间隔 > 前 70%），若不符合此特征（如匀速或减速），则判定为异常。 public ApiResponse<?> afterValid(Boolean basicValid, ImageCaptchaTrack imageCaptchaTrack, AnyMap captchaValidData, Float tolerant, String type) { // 如果基础验证未通过，直接返回成功（无需二次验证） if (!basicValid) { return ApiResponse.ofSuccess(); // 如果不是滑块验证码类型，直接返回成功（无需验证） } else if (!CaptchaTypeClassifier.isSliderCaptcha(type)) { return ApiResponse.ofSuccess(); } long startSlidingTime = imageCaptchaTrack.getStartTime().getTime(); // 滑动开始时间 long endSlidingTime = imageCaptchaTrack.getStopTime().getTime(); // 滑动结束时间 Integer bgImageWidth = imageCaptchaTrack.getBgImageWidth(); // 背景图宽度 List<ImageCaptchaTrack.Track> trackList = imageCaptchaTrack.getTrackList(); // 滑动轨迹点列表 // 如果滑动时间小于300毫秒（太快），判定为机器操作 if (startSlidingTime + 300L > endSlidingTime) { return ApiResponse.ofMessage(DEFINITION); } // 轨迹点数量需在 [10, 背景图宽度*5] 范围内 else if (trackList.size() >= 10 && trackList.size() <= bgImageWidth * 5) { // 轨迹点数量合理，进入下一步验证 } else { // 轨迹点太少（<10）或太多（>宽度*5），判定为异常 return ApiResponse.ofMessage(DEFINITION); } ImageCaptchaTrack.Track firstTrack = trackList.get(0); // 第一个轨迹点 // 检查初始位置是否在 [(-10,-10), (10,10)] 范围内（接近起点） if (!(firstTrack.getX() > 10.0F) && !(firstTrack.getX() < -10.0F) && !(firstTrack.getY() > 10.0F) && !(firstTrack.getY() < -10.0F)) { // 初始位置合理，进入下一步验证 } else { // 初始位置偏离起点过远，判定为异常 return ApiResponse.ofMessage(DEFINITION); } int check4 = 0; // 记录Y坐标与初始Y相同的轨迹点数量 int check7 = 0; // 记录X坐标超出背景图宽度的轨迹点数量 for(i = 1; i < trackList.size(); ++i) { track = trackList.get(i); x = track.getX(); y = track.getY(); // 统计Y坐标未变化的点（机器可能水平滑动，Y值不变） if (firstTrack.getY() == y) { ++check4; } // 统计X坐标超出背景图宽度的点（异常越界） if (x >= bgImageWidth) { ++check7; } // 检查相邻轨迹点的位移是否过大（单次移动>50像素） preTrack = trackList.get(i - 1); if (track.getX() - preTrack.getX() > 50.0F || track.getY() - preTrack.getY() > 50.0F) { return ApiResponse.ofMessage(DEFINITION); // 位移过大，判定为异常 } } // 条件1：Y坐标不变的点数量≠总轨迹点数量（排除完全水平滑动） // 条件2：越界点数量≤200（允许少量异常，但不能过多） if (check4 != trackList.size() && check7 <= 200) { // 取轨迹的前70%和后30%，计算两段的平均时间间隔 int i = (int)(trackList.size() * 0.7); ImageCaptchaTrack.Track track70 = trackList.get(i - 1); float avgTimeFirst70 = track70.getT() / i; // 前70%轨迹的平均时间 ImageCaptchaTrack.Track lastTrack = trackList.get(trackList.size() - 1); double avgTimeLast30 = lastTrack.getT() / (trackList.size() - i); // 后30%轨迹的平均时间 // 真人滑动通常会越到后面越快（平均时间间隔增大） if (avgTimeLast30 > avgTimeFirst70) { return ApiResponse.ofSuccess(); // 符合真人特征，验证通过 } else { return ApiResponse.ofMessage(DEFINITION); // 速度变化异常，验证失败 } } else { return ApiResponse.ofMessage(DEFINITION); // 异常点比例过高，验证失败 } }
前言 由于国内法律法规的限制，免费的SSL证书由1年变成了3个月，对于在弄了一个带有自己域名博客的人来说，这是一件比较无语的事情，每3个月重复性的工作需要手动更换SSL证书。网上也有很多docker版本的自动续签的应用，如宝塔，acme.sh等等。但是安装这种软件的话，功能就多了许多其它的，个人又不需要。因此自己简单弄一个脚本去实现就十分必要。 操作过程 1、首先需要在工作台创建一个子账号，并取得账号分配的key与secret，给子用户分配AliyunDNSFULLAccess与AliyunYunDunCertAccess权限。 2、生成2048位的crs证书，一般以RSA算法为主，通过Linux系统中自带的openssl命令生成即可 #openssl req -new -nodes -sha256 -newkey rsa:2048 -keyout blog.key -out blog.csr -config openssl.cnf [req] default_bits = 2048 prompt = no default_md = sha256 distinguished_name = dn req_extensions = req_ext [dn] CN = aiwin.net.cn emailAddress = test@foxmail.com O = zirong OU = liu L = Guangzhou ST = Guangdong C = CN [req_ext] subjectAltName = @alt_names [alt_names] DNS.1 = aiwin.net.cn DNS.2 = www.aiwin.net.cn 3、通过crontab定时任务每3个月执行脚本，注意脚本中的crs需要带上\r\n或\n，最好通过一行直接写完，否则传输到阿里云接口会因为没有固定格式的分行而报错。 import os import time import logging from alibabacloud_cas20200407.client import Client as cas20200407Client from alibabacloud_credentials.client import Client as CredentialClient from alibabacloud_tea_openapi import models as open_api_models from alibabacloud_cas20200407 import models as cas_20200407_models from alibabacloud_tea_util import models as util_models from alibabacloud_alidns20150109.client import Client as Alidns20150109Client from alibabacloud_alidns20150109 import models as alidns_20150109_models logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') ''' 全局存储OrderId与RecordId ''' OrderIdList = [] RecordIdList = [] class SSL: def __init__(self, client, endpoint): self.client = self.create_client(client, endpoint) ''' 环境变量设置id与secret,根据不同的方法与endpoint创建不同的安全功能客户端 @ALIBABA_CLOUD_ACCESS_KEY_ID @ALIBABA_CLOUD_ACCESS_KEY_SECRET ''' @staticmethod def create_client(func, endpoint): credential = CredentialClient() config = open_api_models.Config( credential=credential, region_id='cn-hangzhou', endpoint=endpoint, connect_timeout=5000, read_timeout=5000 ) return func(config) ''' 请求创建SSL证书并获取订单ID ''' @staticmethod def main(self): create_certificate_for_package_request_request = cas_20200407_models.CreateCertificateForPackageRequestRequest( csr='''-----BEGIN CERTIFICATE REQUEST-----\r\n内容\r\n-----END CERTIFICATE REQUEST-----''', product_code='digicert-free-1-free', username='xxxx', phone='xxxx', email='test@foxmail.com', domain='aiwin.net.cn', validate_type='DNS' ) runtime = util_models.RuntimeOptions() try: response = self.client.create_certificate_for_package_request_with_options( create_certificate_for_package_request_request, runtime) OrderIdList.append(response.body.order_id) self.parseOrderId(self) except Exception as error: logging.error(error) ''' 通过订单Id,请求SSL证书详情,获取验证时需要设置的域名与TXT记录,并且每相隔60秒请求一次是否已签发接口,签发即删除DNS记录并获取pem与key ''' @staticmethod def parseOrderId(self): describe_certificate_state_request = cas_20200407_models.DescribeCertificateStateRequest( order_id=OrderIdList[0] ) runtime = util_models.RuntimeOptions() try: response = self.client.describe_certificate_state_with_options(describe_certificate_state_request, runtime) self.addDNS(self, response.body.record_domain, response.body.record_value) IssuedResult = self.ConfirmSSLIssued(self) while not IssuedResult: time.sleep(60) IssuedResult = self.ConfirmSSLIssued(self) self.delDNS(self) self.getPrivatePem(self) except Exception as error: logging.error(error) ''' 添加DNS记录 ''' @staticmethod def addDNS(self, domain, value): self.client = self.create_client(Alidns20150109Client, 'alidns.cn-hangzhou.aliyuncs.com') add_domain_record_request = alidns_20150109_models.AddDomainRecordRequest( lang='zh', type='TXT', value='wdadawdawd.txt', rr='_dnsauth.aiwin.net.cn', # value=value, # rr=domain, domain_name='aiwin.net.cn' ) runtime = util_models.RuntimeOptions() try: response = self.client.add_domain_record_with_options(add_domain_record_request, runtime) RecordIdList.append(response.body.record_id) except Exception as error: logging.error(error) ''' 根据RecordId删除DNS记录,并清空RecordId ''' @staticmethod def delDNS(self) -> None: self.client = self.create_client(Alidns20150109Client, 'alidns.cn-hangzhou.aliyuncs.com') delete_domain_record_request = alidns_20150109_models.DeleteDomainRecordRequest( record_id=RecordIdList[0], lang='zh' ) runtime = util_models.RuntimeOptions() try: response = self.client.delete_domain_record_with_options(delete_domain_record_request, runtime) if response.status_code == 200: RecordIdList.clear() print(RecordIdList) except Exception as error: logging.error(error) ''' 验证创建的证书是否已签发状态 ''' @staticmethod def ConfirmSSLIssued(self): self.client = self.create_client(cas20200407Client, 'cas.aliyuncs.com') list_user_certificate_order_request = cas_20200407_models.ListUserCertificateOrderRequest(status='ISSUED') runtime = util_models.RuntimeOptions() try: response = self.client.list_user_certificate_order_with_options(list_user_certificate_order_request, runtime) for ResponseBodyCertificateOrderList in response.body.certificate_order_list: if OrderIdList[0] == ResponseBodyCertificateOrderList.order_id: return True return False except Exception as error: logging.error(error) ''' 根据订单ID,查看证书的详情,并取得certificate与private_key,即nginx文件中的pem和key内容 ''' @staticmethod def getPrivatePem(self): self.client = self.create_client(cas20200407Client, 'cas.aliyuncs.com') describe_certificate_state_request = cas_20200407_models.DescribeCertificateStateRequest( order_id=OrderIdList[0] ) runtime = util_models.RuntimeOptions() try: response = self.client.describe_certificate_state_with_options(describe_certificate_state_request, runtime) self.CreateFile(self, "\n".join(response.body.certificate.splitlines()), "\n".join(response.body.private_key.splitlines())) except Exception as error: logging.error(error) ''' 移除原文件,将新内容写入到nginx.conf路径文件当中 ''' @staticmethod def CreateFile(self, pem_body, key_body): try: os.remove('nginx中pem的路径') os.remove('nginx中key的路径') KeyFile = open('替换为域名.key', 'a') PemFile = open('替换为域名.pem', 'a') KeyFile.write(key_body) PemFile.write(pem_body) logging.info('pem与key文件创建成功') except Exception as error: logging.error(error) if __name__ == '__main__': ssl = SSL(cas20200407Client, 'cas.aliyuncs.com') ssl.main(ssl)
前言 简单的入门一下Go、会对基础语法、网络编程、Gin开发进行简单的了解，关键是Gin开发。 关于Go Go语言（也称为Golang）是Google开发的一种开源编程语言。它被设计用于构建高效、可靠和可扩展的软件系统。下面是Go语言的一些主要用途： 服务器端开发：Go语言提供了强大的标准库和并发模型，使其成为构建高性能网络服务器的理想选择。许多大型互联网公司正在使用Go语言来开发后端服务，以处理高负载和并发请求。 网络编程：Go语言提供了丰富的网络编程库，可用于开发各种网络应用程序，包括Web服务、API服务器、网络代理等。 分布式系统：Go语言的并发模型和原生支持的并发原语（goroutine和channel）使其非常适合构建分布式系统，例如数据处理管道、消息队列等。 命令行工具：Go语言的编译速度快，生成的可执行文件体积小，使其成为开发命令行工具的良好选择。许多开发者使用Go语言来构建工具、脚本和自动化任务。 嵌入式系统：Go语言可以用于编写嵌入式系统的控制逻辑和驱动程序。它提供了对底层硬件的访问和控制能力，并具有较小的内存消耗。 需要注意的是，Go语言具有简洁而直观的语法，易于学习和使用。它的性能非常好，可以充分利用多核处理器和并发编程来提高应用程序的性能和吞吐量。 学习流程 基础语法->Web开发->常见中间件->云平台 基础语法 注释 增强语言的可读性 单行注释 多行注释 package main import "fmt" // 单行注释 /* 多行注释 多行注释 */ func main() { fmt.Println("hello world") } 变量 var 定义变量，var 变量名 变量类型。 简短变量声明，使用:=运算符可以在函数内部声明并初始化变量。 匿名变量，使用 _ 占位符可以声明一个匿名变量，忽略不需要的值，任何赋值給这个标识符的值都将被抛弃，并且不会导致变量的冲突 定义多个变量，可以使用()包裹，表示定义多个变量 注意点：变量名的首个字符不能为数字，全局变量可被局部变量再定义(就近原则 )，定义的变量一定要使用。 变量声明后的默认值： 整数型浮点数变量默认值是0和0.0 字符串变量默认值是空字符串 布尔型变量默认是false 切片，函数，指针变量默认是nil Printf输出时声明的格式 %v：默认格式化输出，会根据变量的类型自动选择合适的格式。 %s：输出字符串。 %d、%b、%o、%x：输出整数，分别表示十进制、二进制、八进制和十六进制。 %t：输出布尔值，结果为 true 或 false。 %f、%e、%g：输出浮点数，分别表示十进制表示法、科学计数法和通用格式。 %p：输出指针地址。 %c：输出字符。 %q：输出带引号的字符串。 %%：输出一个百分号。 fmt.Printf("内存地址:%p,变量类型:%T",name,&name) //打印内存地址，变量类型等 例子： package main import "fmt" var name = "Lau" //全局变量(隐式定义) func fun() (int, int) { return 100, 200 } func main() { var name string = "aiwin" //显示定义 var a, b int //同时定义a,b两个变量 age := 18 fmt.Printf("姓名:%s,内存地址为:%p,类型为:%T,年龄:%d,年龄十六进制数为:%x\n", name, &name, name, age, age) //就近原则 a, _ = fun() _, b = fun() fmt.Println("_可代替被舍弃的值,并且可被重复定义:", a, b) a, b = b, a fmt.Println("类似于Python,可直接进行值交换:", a, b) } 常量 使用const来定义常量，不可改变 iota 开始是0，默认会不断的自增进行计数，相当于是一个常量的计数器，直至新的一组常量计数器出现才会恢复，可理解过const语句块的索引 package main import "fmt" func main() { const ( a = iota b c d = "aiwin" e //未被定义所以用上一个的值,但是iota还是会一直计数 f = 100 g //未被定义所以用上一个的值 h = iota j ) const ( k = iota //新的const出现,新iota被重新从0开始计数 l ) fmt.Println(a, b, c, d, e, f, g, h, j, k, l) //0 1 2 aiwin aiwin 100 100 7 8 0 1 } 数据类型 布尔型bool，默认值是false 数字类型，分为int和float，并且支持复数，位运算采用补码 序号 类型和描述 1 uint8无符号8位整型(0~255) 2 uint16无符号16位整型(0~65535) 3 uint32无符号32位整型(0~4294967295) 4 uint64无符号64位整型(0~18446744073709551615) 5 int8有符号8位整型(-128~127)i 6 int16符号16位整型(-32768~32767) 7 int32有符号32位整型(-2147483648~2147483647) 8 int64有符号64位整型(-9223372036854775808~9223372036854775807) float浮点型，默认是64位，保留6位小数，保留小数会丢失精度，采取四舍五入的原则 序号 类型和描述 1 float32 IEEE-754 32位浮点型数 2 float64 IEEE-754 64位浮点型数 3 complex64 32 位实数和虚数 4 complex128 64 位实数和虚数 类型别名，Go语言会有一些类型的别名 序号 类型和描述 1 byte类似uint8 2 int和uint一样大小 3 rune类似int32 7 uintptr无符号整型，用于存放指针 字符类型，' 和" 和双引号包裹的字符是有差别的，' 默认是int32 类型，会自动转换成Unicode 编码的值，字符可以直接使用+ 连接 类型转换 ，Go语言不存在隐式类型转换，所有的类型转换都必须是显式的声明 package main import ( "fmt" ) func main() { var age byte = 18 //相当于uint8 //超过范围,报错 age = 9223372036854775808 var num1 float32 = -123.0000901 var num2 float64 = -123.0000901 fmt.Println("num1=", num1, "num2=", num2) //精度缺失 fmt.Println("转换后导致的精度丢失:num=", float32(num2)) var num3 float64 = 3.19 fmt.Printf("num3=%.1f\n", num3) //四舍五入,输出3.2 fmt.Printf("age的类型为%T,数值为%d", age, age) str := "Hello" str1 := '中' //使用Unicode编码表,会自动认为是int32类型 str2 := "World" fmt.Printf("%T,%s\n", str, str) fmt.Printf("%T,%d\n", str1, str1) //默认是int32类型,转换成数字 fmt.Println(str + "," + str2) var b uint16 = 256 fmt.Println("b=", uint8(b)) //变成了0 /* flag := 2 fmt.Println(bool(flag)) 整型不能转换成bool类型*/ } 运算符 运算符 描述 && 逻辑 AND 运算符。 如果两边的操作数都是 True，则条件 True，否则为 False。 双竖线 逻辑 OR 运算符。 如果两边的操作数有一个 True，则条件 True，否则为 False。 ! 逻辑 NOT 运算符。 如果条件为 True，则逻辑 NOT 条件 False，否则为 True。 运算符 描述 & 按位与运算符"&"是双目运算符。都是1结果为1，否则为0 竖线 按位或运算符"竖线"是双目运算符。 都是0结果为0，否则为1 ^ 按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或，当两对应的二进位相异时，结果为1。 << 左移运算符"<<"是双目运算符。左移n位就是乘以2的n次方。 其功能把"<<"左边的运算数的各二进位全部左移若干位，由"<<"右边的数指定移动的位数，高位丢弃，低位补0。 >> 右移运算符">>"是双目运算符。右移n位就是除以2的n次方。 其功能是把">>"左边的运算数的各二进位全部右移若干位，">>"右边的数指定移动的位数。 &^ 位清空，a&^b，对于b上的每个数值，如果为0，则取a对应位上的数，如果为1，则取0 运算符 描述 & 返回变量存储地址 * 指针变量。 package main import "fmt" func main() { var a uint = 13 //0011 1100 var b uint = 60 //0000 1101 fmt.Println(a & b) //0000 1100 fmt.Println(a | b) //0011 1101 fmt.Println(a ^ b) //0011 0001 fmt.Println(a &^ b) // 0000 0001 fmt.Println(b >> a) //60右移60位,结果为0 } fmt库 常用的一些函数 Print / Println / Printf：格式化并输出到标准输出。 Println：类似于 Print，但在输出后添加换行符。 Printf：使用格式化字符串进行输出（类似于 C 语言中的 printf 函数）。 Sprint / Sprintln / Sprintf：将格式化的结果以字符串形式返回，而不是输出到标准输出。 Fprint / Fprintln / Fprintf：将格式化的结果输出到指定的文件（io.Writer）。 Errorf：生成一个格式化的错误字符串。 Scan / Scanln / Scanf：从标准输入读取并格式化输入。 Sscan / Sscanln / Sscanf：从给定的字符串中读取并格式化输入。 Fscan / Fscanln / Fscanf：从指定的文件（io.Reader）中读取并格式化输入。 package main import ( "fmt" "os" ) func main() { name := "Alice" age := 30 height := 1.68 // 格式化并输出到标准输出 fmt.Print("Hello, ") fmt.Print(name) fmt.Println("!") // 使用格式化字符串进行输出 fmt.Printf("%s is %d years old.\n", name, age) // 输出到指定文件 file, _ := os.Create("user.gob") defer file.Close() fmt.Fprintln(file, name) fmt.Fprintf(file, "%d", age) //标准化读取文件输入 file, _ = os.Open("user.gob") defer file.Close() var ReadName string var ReadAge int fmt.Fscanln(file, &ReadName) //以行为单位来读取 fmt.Fscanln(file, &ReadAge) fmt.Printf("读取到的数据为,Name: %s,Age: %d\n", ReadName, ReadAge) // 将格式化的结果以字符串形式返回 info := fmt.Sprintf("Name: %s, Age: %d, Height: %.2f", name, age, height) fmt.Println(info) // 从标准输入读取并格式化输入 var input string fmt.Print("Enter your name: ") fmt.Scanln(&input) fmt.Printf("Hello, %s!\n", input) } 流程控制 if、switch、select 语句 描述 if语句 if 语句 由一个布尔表达式后紧跟一个或多个语句组成。 if else if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。 else if 你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。 switch switch 语句用于基于不同条件执行不同动作。 fallthrough 当使用swich语句时，可以使用fallthrough 进行case穿透，下面的条件一定会执行 select select 语句类似于 switch 语句，但是select会随机执行一个可运行的case。如果没有case可运行，它将阻塞，直到有case可运行。 package main import ( "fmt" "time" ) func main() { //if语句 var password string var username string fmt.Print("请输入账号:") fmt.Scan(&username) fmt.Print("请输入密码:") fmt.Scan(&password) if username == "admin" { if password == "Qyx@Sxf715" { fmt.Println("登录成功") } else { fmt.Println("密码错误") } } else { fmt.Println("用户名错误") //注意这里的else一定要接在if的}后面,不能进行换行 } //switch var score int = 88 switch { case score >= 90: fmt.Println("成绩为A级") case score >= 80 && score < 90: fmt.Println("成绩为B级") //fallthrough 一定会把下面的一个case也穿透掉 case score >= 70 && score < 80: fmt.Println("成绩为C级") case score >= 60 && score < 70: fmt.Println("成绩为D级") default: fmt.Println("成绩为不及格") } //select语句的使用 ch1 := make(chan string) //创建了两个通道 ch1 和 ch2 ch2 := make(chan string) //两个匿名的 goroutine 分别向这两个通道发送值 go func() { time.Sleep(2 * time.Second) ch1 <- "Hello" }() go func() { time.Sleep(3 * time.Second) ch2 <- "World" }() /*select 会同时监听多个通道的操作，当任何一个 case 中的操作就绪时，该 case 就会被执行。 如果同时有多个 case 就绪，select 随机选择一个可执行的 case 来执行。 如果没有任何 case 就绪，并且存在 default 分支，那么执行 default 分支。*/ select { case msg1 := <-ch1: fmt.Println("Received from ch1:", msg1) case msg2 := <-ch2: fmt.Println("Received from ch2:", msg2) case <-time.After(5 * time.Second): fmt.Println("Timed out") } } for、break、continue Go语言的for循环也需要三个参数，起始位，最终位，间距 ，但是三个参数都可以省略掉。 package main import "fmt" func main() { //9*9乘法表 for j := 1; j <= 9; j++ { for i := 1; i <= j; i++ { fmt.Printf("%dx%d=%d \t", i, j, i*j) } fmt.Println() } for j := 1; j <= 9; j++ { if j == 5 { //结束掉整个循环 break } fmt.Print(j) } fmt.Println() for j := 1; j <= 9; j++ { if j == 5 { //结束当次循环 continue } fmt.Print(j) } } 遍历String package main import "fmt" func main() { var str string = "Hello,Aiwin" fmt.Println(str) fmt.Printf("字符串的长度为%d\n", len(str)) fmt.Printf("第二个字符是%c\n", str[1]) //遍历字符串 for i := 0; i < len(str); i++ { fmt.Printf("%c", str[i]) } //for range fmt.Println("\n") for i, v := range str { fmt.Printf("%d%c\n", i, v) } } 函数 函数是一个基本代码块，用于执行一个任务 Go语言最少有一个main函数 函数声明告诉编译器函数的名称，返回类型，参数 函数本身也是一个变量，也可以进行赋值 function 函数名(参数，参数类型)(返回类型){} 形式参数：定义函数时，用于接收外部传入数据的参数 实际参数：调用函数时，传给形参的实际数据是实际参数 可变参数：参数类型确定，但是数量不确定，可以用...，可变参数前面可以继续定义参数，后面不能再定义参数，一个函数列表只能有一个可变参数 package main import ( "fmt" ) func main() { fmt.Println("1+2的结果为", add(1, 2)) x, y := swap("你好", "Go语言") fmt.Println(x, y) printMessage("一个参数的函数") printStatic() fmt.Println("求和函数:", getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) } // 有多个返回值函数 func swap(x, y string) (string, string) { return y, x } // 有两个参数的函数 func add(a, b int) int { result := a + b return result } // 一个参数函数 func printMessage(msg string) { fmt.Println(msg) } // 无参数函数 func printStatic() { fmt.Println("无参数函数") } func getSum(number ...int) int { sum := 0 for i := 0; i < len(number); i++ { sum += number[i] } return sum } 值传递和引用传递 值类型数据：操作的是数据本身，如int，string ，bool，array，改变值不会改变数据本身，地址是不一样的 引用类型数据，操作的是数据的地址，如slice，map ，chanel，改变的时候会一起改变，函数地址是一样的。 defer 可以在函数中添加多个defer语句，当函数执行到最后时，这些defer语句会逆序执行，可以用于在函数返回前关闭相应的资源等操作 package main import "fmt" func main() { //值传递 arr := [4]int{1, 2, 3, 4} updateArr(arr) fmt.Println(arr) //引用传递 sli := []int{1, 2, 3, 4, 5} updateSlice(sli) fmt.Println(sli) //defer函数 a := 10 defer MyPrint(a) //输出10,已经传递进去了,一切准备就绪 a++ fmt.Println(a) } func updateArr(arr [4]int) { arr[1] = 100 fmt.Println(arr) } func updateSlice(sli []int) { sli[1] = 100 fmt.Println(sli) } func MyPrint(number int) { fmt.Println(number) } init init()函数不能被其它函数调用，而是在main函数执行之前自动被调用 init 函数不能作为参数传入，不能有传入参数和返回值 当一个main有多个init函数，谁在前谁就先执行 匿名、回调、闭包函数 也叫闭包函数(closures),允许临时创建一个没有指定名称的函数 回调函数就是一个函数作为另一个函数的参数。 闭包结构，一个外层函数中有内层函数，该内层函数中可以操作外层函数的局部变量，并且外层函数的返回值是内层函数，这种结构是闭包结构。 正常的局部变量会随着函数的调用而创建，随着函数的结束而销毁，但是在闭包结构中的外层函数的局部变量并不会随着外层函数的结果而销毁，因为内层函数还在堆栈中使用 package main import "fmt" func main() { r1 := operator(1, 2, add) fmt.Println(r1) //匿名函数 r2 := operator(3, 2, func(a, b int) int { if b == 0 { return 0 } return a / b }) fmt.Println(r2) //闭包结构 f1 := increment() fmt.Println(f1()) fmt.Println(f1()) f2 := increment() fmt.Println(f2()) fmt.Println(f1())//输出3,f1没有被销毁 } // 回调函数,fun是告诫函数,operator是回调函数 func operator(a, b int, fun func(int, int) int) int { return fun(a, b) } func add(a, b int) int { return a + b } func increment() func() int { i := 0 fun := func() int { i++ return i } return fun } 数组和切片 数组的数量在创建的时候就定义的，不可再进行改变。 数组的定义语法： var 数组名 [数量]数据类型=[数量]数据类型{数据} 切片相对于数组来说更加灵活，切片的长度是可变的，相当于可切的数组 package main import "fmt" func main() { var nameArr [3]string = [3]string{"Hello", "Wor", "ld"} fmt.Println(nameArr) var nameList []string nameList = append(nameList, "Hello") fmt.Println(nameList) } Map Map就是Python中的字典(键值对)，Map创建的时候一定需要初始化 var 变量名 map[键类型]值类型{} package main import "fmt" func main() { var userMap map[int]string = map[int]string{ 1: "Aiwin", 2: "Lau", 3: "", } fmt.Println(userMap[1]) value, ok := userMap[3] //3存在,ok是true fmt.Println(value, ok) userMap[1] = "LauAiwin" delete(userMap, 3) fmt.Println(userMap) } 结构体 结构体定义，可以类比为对象 type 结构体名称 struct{ 名称 类型 } package main import ( "encoding/json" "fmt" ) type Parent struct { Name string Age int } type Children struct { Parent Name string Age int } func (children *Children) setChildernName(name string, age int) { //引用传递 children.Name = name children.Age = age } type User struct { Username string `json:"username"` //转换结果为username Password string `json:"-"` //不显示 Age int `json:"age,omitempty"` //抛弃空值 } func main() { parent := Parent{Name: "Lau", Age: 40} childern := Children{parent, "Aiwin", 20} fmt.Printf("%s的父亲是:%s,年龄是:%d\n", childern.Name, childern.Parent.Name, childern.Parent.Age) childern.setChildernName("LauAiwin", 19) fmt.Println(childern) user := User{"Aiwin", "123456", 0} byteData, _ := json.Marshal(user) fmt.Println(string(byteData)) } 自定义数据类型 自定义类型的本意就是为了代码更简化、易于理解、方便维护。 类型别名(就是将类型赋值给一个type) 不能绑定方法 打印类型还是原始类型 类型别名不用转换 package main import "fmt" type Code int const ( SuccessCode Code = 1 ErrorCode Code = 2 ) func (code Code) getMessage() (message string) { switch code { case SuccessCode: return "请求成功" case ErrorCode: return "请求失败" } return "" } func (code Code) result() (result Code, message string) { return code, code.getMessage() } func Request(name string) (code Code, message string) { if name == "admin" { return SuccessCode.result() } else { return ErrorCode.result() } } func main() { fmt.Println(Request("admin")) } 接口 接口是一组仅包含方法名、参数、返回值的为具体实现的方法的集合，同样接口也不能绑定方法 package main import "fmt" type Student struct { name string } type Name interface { getName() string } func (student Student) getName() string { return student.name } type Teacher struct { name string } func (teacher Teacher) getName() string { return teacher.name } // 接口,可以统一传入其它的类型 func getName(name Name) string { //name.(Teacher) switch types := name.(type) { //类型断言 case Teacher: fmt.Println(types) case Student: fmt.Println(types) } return name.getName() } func MyPrint(val interface{}) { //空接口 fmt.Println(val) } func main() { student := Student{"Aiwin"} teacher := Teacher{"Xd"} fmt.Println(getName(student)) fmt.Println(getName(teacher)) MyPrint(1) } 协程和channel 协程可以理解为轻量级线程，一个线程可以拥有多个协程，与线程相比，协程不受操作系统调度，协程调度器按照调度策略把协程调度到线程中执行，协程调度器由应用程序的runtime包提供，用户使用go关键字即可创建协程，这也就是GO在语言层面直接支持协程的特色 package main import ( "fmt" "sync" "time" ) func shopping(name string, group *sync.WaitGroup) { fmt.Printf("%s 开始攻击\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 停止攻击\n", name) group.Done() } func main() { var group sync.WaitGroup StartTime := time.Now() group.Add(3) go shopping("张三", &group) go shopping("李四", &group) go shopping("王五", &group) group.Wait() fmt.Println(time.Since(StartTime)) } 那么协程里面产生的数据，怎么传递给主线程，Go 官方使用channel 来传递 package main import ( "fmt" "sync" "time" ) var moneyChanel chan int = make(chan int) var nameChanel chan string = make(chan string) var DoneChanel chan struct{} = make(chan struct{}) func shopping(name string, money int, group *sync.WaitGroup) { fmt.Printf("%s 开始攻击\n", name) time.Sleep(1 * time.Second) fmt.Printf("%s 停止攻击\n", name) moneyChanel <- money nameChanel <- name group.Done() } func main() { var group sync.WaitGroup StartTime := time.Now() group.Add(3) go shopping("张三", 100, &group) go shopping("李四", 150, &group) go shopping("王五", 160, &group) var moneyList []int var nameList []string go func() { //解决moneyChanel一直死循环的问题 defer close(moneyChanel) defer close(nameChanel) defer close(DoneChanel) group.Wait() }() //go func(){ // for money := range moneyChanel { // moneyList = append(moneyList, money) //解决moneyChannel被一直输数据问题 // } //}() //for name := range nameChanel { // nameList = append(nameList, name) //} event := func() { for { select { case names := <-nameChanel: nameList = append(nameList, names) case money := <-moneyChanel: moneyList = append(moneyList, money) case <-DoneChanel://解决当协程全部完事,退出循环的问题 return } } } event() fmt.Println(moneyList) fmt.Println(nameList) fmt.Println(time.Since(StartTime)) } 超时 package main import ( "fmt" "time" ) var doneChanel = make(chan struct{}) func timeOut() { fmt.Println("开始") time.Sleep(3 * time.Second) fmt.Println("结束") close(doneChanel) } func main() { go timeOut() select { case <-doneChanel: fmt.Printf("执行完成") case <-time.After(4 * time.Second): fmt.Println("超时") return } } 线程锁 package main import ( "fmt" "sync" ) var sum int var wait sync.WaitGroup var lock sync.Mutex func add() { lock.Lock() //线程锁,不然会线程紊乱 for i := 0; i < 10000; i++ { sum++ } lock.Unlock() wait.Done() } func sub() { lock.Lock() for i := 0; i < 10000; i++ { sum-- } lock.Unlock() wait.Done() } func main() { wait.Add(2) go add() go sub() wait.Wait() fmt.Println(sum) var maps = sync.Map{}//Map的协程紊乱,要使用这种创建方式 go func() { for { maps.Store(1, "Aiwin") } }() go func() { for { fmt.Println(maps.Load(1)) } }() select {} } 异常处理 Go语言没有捕获异常的机制，每次都要接error ，这是Go语言的一个诟病，异常处理可分为三种，分别是中断、恢复、从上一级返回处理。 例子： package main import ( "errors" "fmt" ) // 中断仅适用于init开始时 // // func init() { // _, err := os.ReadFile("aaa") // if err != nil { // panic("中断报错了") // } // } func div(a, b int) (res int, err error) { if b == 0 { err = errors.New("除数不能为0") return 0, err } res = a / b return res, nil } func zhixing() (res int, err error) { res, err = div(2, 0) if err != nil { return 0, err } res += 2 return res, nil } func recovery() { defer func() { recover() }() var lists []int = []int{1, 2, 3} fmt.Println(lists[4]) } func main() { /*错误向上处理 res, err := zhixing() if err != nil { fmt.Println(err) return } fmt.Println(res)*/ recovery() fmt.Println("恢复正常逻辑") } 泛型 Go语言的泛型是指在定义函数、数据结构或接口时，可以不指定具体数据类型，而是以一种通用的方式编写代码，以便在不同的数据类型上有效地进行操作。泛型使得代码更具有通用性和可复用性，因为它可以适用于多种不同类型的数据而无需重复编写相似的代码。 比如说结构的泛型： package main import ( "encoding/json" "fmt" ) type Response[A any] struct { Code int `json:"code"` Msg string `json:"msg"` Data A `json:"data"` } type User struct { Name string `json:"name"` } type User1 struct { Name string `json:"name"` Age int `json:"age"` } func main() { //UserInfo := Response[User]{ // Code: 1, // Msg: "反序列化", // Data: User{ // Name: "Aiwin", // }, //} //marshal, _ := json.Marshal(UserInfo) //fmt.Println(string(marshal)) var UnMarRes Response[User] json.Unmarshal([]byte(`{"code":1,"msg":"反序列化","data":{"name":"Aiwin"}}`), &UnMarRes) //通过泛型可以识别是属于哪一个User fmt.Println(UnMarRes.Data.Name) } 文件读取 os.ReadFile()一次性读取整个文件的内容。 os.Open()分片读取。 bufio依赖来读取，指定分割符，换行符等 package main import ( "bufio" "fmt" "os" ) func main() { file, err := os.Open("text.txt") if err != nil { panic("文件读取错误") } //buf := bufio.NewReader(file) //for { // line, _, err := buf.ReadLine() // if err == io.EOF { // break // } // fmt.Println(string(line)) //} //指定分割符 scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) for scanner.Scan() { fmt.Println(scanner.Text()) } } 文件写入 os.openFile()中flag的类型： const ( O_RDONLY int = syscall.O_RDONLY // open the file read-only. O_WRONLY int = syscall.O_WRONLY // open the file write-only. O_RDWR int = syscall.O_RDWR // open the file read-write. O_APPEND int = syscall.O_APPEND // append data to the file when writing. O_CREATE int = syscall.O_CREAT // create a new file if none exists. O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist. O_SYNC int = syscall.O_SYNC // open for synchronous I/O. O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened. ) 例子: package main import ( "fmt" "io" "os" ) func main() { //文件写入 file, err := os.OpenFile("text.txt", os.O_CREATE|os.O_RDWR, 0777) //模式和权限,权限仅针对linux系统 if err != nil { panic("文件写入失败") } defer file.Close() file.Write([]byte("hello")) //文件写入 err1 := os.WriteFile("text.txt", []byte("password"), 0777) //全部写入,会全覆盖 fmt.Println(err1) //文件复制 rFile, err2 := os.Open("text.txt") if err2 != nil { panic("文件不可取") } wFile, err3 := os.OpenFile("copy.txt", os.O_CREATE|os.O_WRONLY, 0777) if err3 != nil { panic("文件错误") } defer wFile.Close() io.Copy(wFile, rFile) //文件复制 dir, err := os.ReadDir("基础语法") if err != nil { panic("error") } for _, entry := range dir { info, _ := entry.Info() fmt.Println(entry.IsDir(), entry.Name(), info.Size()) } } 反射 reflect.typeof 获取类型 reflect.Valueof 获取值 setInt、setString 重新设置值 demo package main import ( "fmt" "reflect" ) func getType(obj any) { v := reflect.TypeOf(obj) switch v.Kind() { case reflect.Int: fmt.Println("获取到Int类型") case reflect.String: fmt.Println("获取到String类型") } } //注意要更改值，需要使用指针的形式更改 func setValue(obj any, value any) { v1 := reflect.ValueOf(obj) v2 := reflect.ValueOf(value) if v1.Elem().Kind() != v2.Kind() { //获取一个值的指针所指向的元素值 return } switch v1.Elem().Kind() { case reflect.Int: v1.Elem().SetInt(v2.Int()) case reflect.String: v1.Elem().SetString(value.(string)) } } func main() { var name = "张三" var age = 24 getType(name) getType(age) setValue(&name, "李四") setValue(&age, 25) fmt.Println(name, age) } package main import ( "fmt" "reflect" ) type User struct { UserName string `json:"userName"` Password string `json:"password"` } func demo(obj any) { t := reflect.TypeOf(obj) v := reflect.ValueOf(obj) for i := 0; i < t.NumField(); i++ { value := v.Field(i) fmt.Println(value) } for i := 0; i < t.NumMethod(); i++ { m := t.Method(i) if m.Name != "Call" { //方法名必须为大写 continue } method := v.Method(i) method.Call([]reflect.Value{ reflect.ValueOf("Aiwin"), }) } } func (User) Call(name string) { fmt.Println("我的名字是", name) } func main() { user := User{UserName: "Aiwin", Password: "123456"} demo(user) } 反射转ord小案例： package main import ( "errors" "fmt" "reflect" "strings" ) type Users struct { Name string `orm:"name"` Id int `orm:"id"` } func Find(obj any, query ...any) (sql string, err error) { t := reflect.TypeOf(obj) if t.Kind() != reflect.Struct { err = errors.New("非结构体") return } //获取到where if len(query) > 0 { // 有第二个参数，校验第二个参数中的？个数，是不是和后面的个数一样 q := query[0] //取第一个参数 if strings.Count(q.(string), "?")+1 != len(query) { err = errors.New("参数个数不对") return } var where string for _, a := range query[1:] { at := reflect.TypeOf(a) switch at.Kind() { case reflect.Int: q = strings.Replace(q.(string), "?", fmt.Sprintf("%d", a.(int)), 1)//将?替换成数值 case reflect.String: q = strings.Replace(q.(string), "?", fmt.Sprintf("'%s'", a.(string)), 1) } } where += "where " + q.(string) //获取到字段 var columns []string for i := 0; i < t.NumField(); i++ { field := t.Field(i) f := field.Tag.Get("orm") columns = append(columns, f) } //获取表名称 table := strings.ToLower(t.Name()) sql = fmt.Sprintf("select %s from %s %s", strings.Join(columns, ","), table, where) } return } func main() { sql, err := Find(Users{}, "name= ? and id = ?", "Aiwin", 1) //select name,id from Users where name='Aiwin' and id=1 fmt.Println(sql, err) sql, err = Find(Users{}, "id = ?", 1) //select name,id from Users where id=1 fmt.Println(sql, err) } TCP网络编程 主要通过net依赖包来完成，比如以下方法： net.Dial(network, address string) (Conn, error)：通过指定的网络协议和地址连接到远程主机，返回一个Conn接口类型的实例和可能的错误。 net.Listen(network, address string) (Listener, error)：在指定的网络协议和地址上监听连接，返回一个Listener接口类型的实例和可能的错误。 net.DialTimeout(network, address string, timeout time.Duration) (Conn, error)：在指定的超时时间内，通过指定的网络协议和地址连接到远程主机，返回一个Conn接口类型的实例和可能的错误。 net.ListenPacket(network, address string) (PacketConn, error)：在指定的网络协议和地址上监听数据包的到达，返回一个PacketConn接口类型的实例和可能的错误。 net.ResolveTCPAddr(network, address string) (*TCPAddr, error)：将字符串形式的TCP地址解析为TCPAddr类型的实例，包括IP地址和端口号等信息。 net.ResolveUDPAddr(network, address string) (*UDPAddr, error)：将字符串形式的UDP地址解析为UDPAddr类型的实例，包括IP地址和端口号等信息。 net.LookupHost(host string) ([]string, error)：通过主机名查询对应的IP地址列表，并返回一个字符串切片和可能的错误。 net.LookupPort(network, service string) (port int, err error)：通过网络协议和服务名查询对应的端口号，并返回端口号和可能的错误。 demo： package main import ( "fmt" "io" "net" ) func main() { tcp, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:81") //将一个字符串形式的 TCP 地址解析为一个 TCPAddr 类型的实例 listen, _ := net.ListenTCP("tcp", tcp) //进行监听 for { //接收连接 fmt.Println("开始监听....") con, err := listen.Accept() if err != nil { break } fmt.Println(con.RemoteAddr().String() + "进来了") for { var buf []byte = make([]byte, 1024) n, err := con.Read(buf) //客户端退出 if err == io.EOF { fmt.Println(con.RemoteAddr().String() + "退出了") break } fmt.Println(string(buf[0:n])) } } } package main import ( "fmt" "net" ) func main() { conn, _ := net.Dial("tcp", "127.0.0.1:81") var s string for { fmt.Scanln(&s) if s == "quit" { break } conn.Write([]byte(s)) } conn.Close() } Http http.HandleFunc(pattern string, handler func(ResponseWriter, *Request))：注册一个处理函数，用于指定URL模式的请求处理。 http.Handle(pattern string, handler http.Handler)：注册一个处理器对象，该对象实现了http.Handler接口，用于指定URL模式的请求处理。 http.ListenAndServe(addr string, handler http.Handler)：启动一个HTTP服务器，监听指定地址，并使用指定的处理器对象处理接收到的请求。 http.Get(url string) (resp *http.Response, err error)：向指定URL发起GET请求，并返回响应结果。 http.Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error)：向指定URL发起POST请求，携带指定类型的请求体，并返回响应结果。 http.NewRequest(method, url string, body io.Reader) (*Request, error)：创建一个自定义的HTTP请求对象，可以指定请求方法、URL和请求体。 http.DefaultServeMux：默认的ServeMux多路复用器，可以通过它来注册处理函数，用于处理HTTP请求。 http.HandlerFunc：将一个函数转换为http.Handler接口的实现，用于处理HTTP请求。 http.Error(w ResponseWriter, error string, code int)：向客户端发送指定状态码的错误响应。 http.Redirect(w ResponseWriter, r *Request, url string, code int)：向客户端发送重定向指令，使其跳转到指定的URL。 demo package main import ( "crypto/md5" "fmt" "net/http" ) func HashUsingMD5(input string) string { hasher := md5.New() hasher.Write([]byte(input)) return fmt.Sprintf("%x", hasher.Sum(nil)) } func LoginHandler(res http.ResponseWriter, req *http.Request) { if req.Method == "POST" { username := req.FormValue("username") password := req.FormValue("password") if username == "admin" && password == "123456" { Cookie := HashUsingMD5(username + password) cookie := http.Cookie{ Name: "Value", Value: Cookie, } http.SetCookie(res, &cookie) http.Redirect(res, req, "/success", http.StatusFound) } else { http.Redirect(res, req, "/", http.StatusFound) } } else { res.WriteHeader(405) res.Write([]byte("Method Not Allow")) } } func SuccessHandler(res http.ResponseWriter, req *http.Request) { cookie, err := req.Cookie("Value") if err != nil { http.Redirect(res, req, "/", http.StatusFound) return } cookie_value := HashUsingMD5("admin123456") if cookie.Value != cookie_value { http.Redirect(res, req, "/", http.StatusFound) } else { res.Write([]byte("登录成功")) } } func main() { //创建一个文件服务器来处理静态文件 fs := http.FileServer(http.Dir("C:\\Users\\25018\\GolandProjects\\Project\\网络编程\\http")) http.Handle("/", http.StripPrefix("/", fs)) http.HandleFunc("/login", LoginHandler) http.HandleFunc("/success", SuccessHandler) fmt.Println("HTTP server running at http://127.0.0.1:7000") err := http.ListenAndServe("127.0.0.1:7000", nil) if err != nil { fmt.Println(err) } } package main import ( "fmt" "net/http" "net/url" ) func main() { client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } res, err := client.PostForm("http://127.0.0.1:7000/login", url.Values{"username": {"admin"}, "password": {"123456"}}) if err != nil { fmt.Println("请求失败") } if res.StatusCode == http.StatusFound { fmt.Println(res.Header) fmt.Println("重定向成功") } } websocket go get github.com/gorilla/websocket websocket是socket连接和http协议的结合体，可以实现网页和服务端的长连接 demo package main import ( "fmt" "github.com/gorilla/websocket" "net/http" ) var UP = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } func handler(res http.ResponseWriter, req *http.Request) { conn, err := UP.Upgrade(res, req, nil) if err != nil { fmt.Println(err) return } for { //消息类型,消息,错误 types, message, err := conn.ReadMessage() if err != nil { break } conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("你说的是:%s吗？", string(message)))) fmt.Println(types, string(message)) } defer conn.Close() fmt.Println("服务关闭") } func main() { http.HandleFunc("/", handler) http.ListenAndServe("127.0.0.1:7000", nil) } package main import ( "bufio" "fmt" "github.com/gorilla/websocket" "os" ) func send(conn *websocket.Conn) { reader := bufio.NewReader(os.Stdin) l, _, _ := reader.ReadLine() conn.WriteMessage(websocket.TextMessage, l) } func main() { dl := websocket.Dialer{} conn, _, err := dl.Dial("ws://127.0.0.1:7000", nil) if err != nil { fmt.Println(err) return } for { go send(conn) t, p, err := conn.ReadMessage() if err != nil { break } fmt.Println(t, string(p)) //1代表TextMessage } } 爬虫 正则表达式 package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "io" "net/http" "os" "regexp" "strconv" "strings" "time" "xorm.io/xorm" ) type Page struct { Id int64 `xorm:"pk autoincr"` //Id自增 Title string Content string `xorm:"text"` CreateTime time.Time `xorm:"created"` UpdatedTime time.Time `xorm:"updated"` } var engine *xorm.Engine func init() { dbType := "mysql" dbHost := "localhost" dbPort := "3306" dbUser := "root" dbPassword := "root" dbName := "test_xorm" // 创建引擎 var err error engine, err = xorm.NewEngine(dbType, dbUser+":"+dbPassword+"@tcp("+dbHost+":"+dbPort+")/"+dbName) //连接mysql数据库 //engine, err = xorm.NewEngine("mysql", "root:root@/test_xorm?charset=utf-8") //连接mysql数据库 if err != nil { fmt.Println(err) panic("初始化不成功") } else { err2 := engine.Ping() if err2 != nil { panic("连接不成功") } else { fmt.Println("连接成功!") } } } func fetch(url string) string { client := &http.Client{} req, _ := http.NewRequest("GET", url, nil) req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36") req.Header.Add("Cookie", "_gid=GA1.2.1321863478.1703508772; _ga_YXBYDX14GJ=GS1.1.1703508771.1.1.1703510246.58.0.0; _ga=GA1.2.1396264026.1703508772") resp, err := client.Do(req) if err != nil { fmt.Println("Http error", err) return "" } if resp.StatusCode != 200 { fmt.Println("Http Status Code", resp.StatusCode) return "" } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("Read error", err) return "" } return string(body) } func parse(html string) { html = strings.Replace(html, "\n", "", -1) re_sidebar := regexp.MustCompile(` (.*?) `) sidebar := re_sidebar.FindString(html) re_link := regexp.MustCompile(`href="(.*?)"`) links := re_link.FindAllString(sidebar, -1) url := "https://gorm.io/zh_CN/docs/" for _, value := range links { fmt.Println(value) href := value[6 : len(value)-1] url := url + href fmt.Println("url\n", url) body := fetch(url) go parse2(body) } } func parse2(body string) { body = strings.Replace(body, "\n", "", -1) re_title := regexp.MustCompile(` (.*?) `) title := re_title.FindString(body) title = title[42 : len(title)-5] fmt.Println("title", title) //save(title, body) saveToDB(title, body) } func save(title string, content string) { err := os.WriteFile("网络编程/Go爬虫/pages/"+title+".html", []byte(content), 0644) if err != nil { panic("保存出现错误") } } func saveToDB(title string, content string) { err := engine.Sync(new(Page)) if err != nil { fmt.Println("Failed to sync database: %v", err) } page := Page{ Title: title, Content: content, } affected, err := engine.Insert(&page) if err != nil { fmt.Println("插入出现错误", err) } fmt.Println("save:" + strconv.FormatInt(affected, 10)) } func main() { url := "https://gorm.io/zh_CN/docs/" html := fetch(url) parse(html) } goquery 通过goquery可以快速的对HTML或XML进行解析，提供简单的API提取一个HTML或XML页面中的节点。 常用元素： Find: 通过CSS选择器查找元素，例如 doc.Find("div.content") 将返回所有class为content的div元素。 Each: 遍历匹配的元素集合，并对每个元素执行指定的函数。 Text: 获取元素的文本内容。 Attr: 获取元素的属性值。 Html: 获取匹配元素的HTML内容。 Parent, Children, Next, Prev: 获取父元素、子元素、相邻的后一个元素、相邻的前一个元素等。 Filter, Not, HasClass, Is: 根据特定条件对元素集合进行过滤和判断。 AddClass, RemoveClass, ToggleClass: 添加、移除、切换元素的类名。 Serialize: 将匹配的表单元素序列化为URL编码的字符串。 Each: 遍历匹配的元素集合，并对每个元素执行指定的函数。 package main import ( "fmt" "github.com/PuerkitoBio/goquery" "net/http" ) func main() { url := "https://gorm.io/zh_CN/docs/" //goquery.NewDocument(url)已过时弃用 res, err := http.Get(url) if err != nil { panic("http请求出现错误") } defer res.Body.Close() doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { panic("goquery解析失败") } doc.Find(".sidebar-link").Each(func(i int, s *goquery.Selection) { href, _ := s.Attr("href") base_url := "https://gorm.io/zh_CN/docs/" detail_url := base_url + href resp, _ := http.Get(detail_url) detail_doc, _ := goquery.NewDocumentFromReader(resp.Body) title := detail_doc.Find(".article-title").Text() content, _ := detail_doc.Find(".article").Html() fmt.Println("title:\n", title) fmt.Println("content\n", content) }) } colly Colly 是一个用于爬取 Web 数据的 Golang 框架，具有以下特点： 简单易用：Colly 提供了一个简洁、直观的 API，易于使用和理解。 高度灵活：Colly 允许你自定义请求头、回调函数、处理方法等，以满足各种爬取需求。 并发支持：Colly 提供了并发请求的支持，可以同时处理多个请求，提高爬取效率。 支持动态网页：Colly 集成了 PhantomJS，可以处理 JavaScript 渲染的动态网页。 内置的选择器引擎：Colly 使用自己的选择器引擎，类似于 CSS 选择器，可以轻松地从 HTML 中提取所需的数据。 自动处理重试和错误：Colly 可以自动处理请求的重试和错误，提供了一种简化错误处理的方法。 支持代理：Colly 允许你使用代理服务器进行请求，以帮助隐藏真实 IP 地址和绕过访问限制。 自定义数据存储：Colly 提供了灵活的机制，允许你自定义数据的存储方式，比如输出到文件、存储到数据库等。 事件驱动：Colly 采用事件驱动的方式，通过注册回调函数处理请求和提取数据。 广泛的社区支持：Colly 作为一个流行的爬虫框架，有着活跃的社区，你可以轻松找到相关的文档、教程和示例代码。 package main import ( "fmt" "github.com/gocolly/colly" ) func main() { c := colly.NewCollector() c.OnHTML(".sidebar-link", func(element *colly.HTMLElement) { href := element.Attr("href") if href != "index.html" { c.Visit(element.Request.AbsoluteURL(href)) } }) c.OnHTML(".article-title", func(element *colly.HTMLElement) { title := element.Text fmt.Println("title:", title) }) c.OnHTML(".article", func(element *colly.HTMLElement) { content, _ := element.DOM.Html() fmt.Println("content:", content) }) c.OnRequest(func(request *colly.Request) { fmt.Println(request.URL.String()) }) url := "https://gorm.io/zh_CN/docs/" c.Visit(url) } 豆瓣250 package main import ( "fmt" "github.com/PuerkitoBio/goquery" _ "github.com/go-sql-driver/mysql" "net/http" "regexp" "strconv" "xorm.io/xorm" ) type MovieData struct { Id int64 `xorm:"pk autoincr"` Title string Year string Score string Director string Actor string `xorm:"text"` Quote string `xorm:"text"` Picture string } var engine *xorm.Engine func init() { dbType := "mysql" dbHost := "localhost" dbPort := "3306" dbUser := "root" dbPassword := "root" dbName := "test_xorm" var err error engine, err = xorm.NewEngine(dbType, dbUser+":"+dbPassword+"@tcp("+dbHost+":"+dbPort+")/"+dbName) if err != nil { fmt.Println(err) panic("初始化失败") } else { err2 := engine.Ping() if err2 != nil { panic("连接不成功") } else { fmt.Println("连接成功!") } } } func main() { for i := 0; i < 10; i++ { fmt.Printf("正常爬取第 %d 页信息\n", i) Spider(strconv.Itoa(i * 25)) } } func Spider(page string) { url := "https://movie.douban.com/top250" + "?start=" + page client := http.Client{} req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36") req.Header.Set("Accept-Encoding", "zh-CN,zh;q=0.9") req.Header.Set("Cache-Control", "max-age=0") resp, _ := client.Do(req) doc, _ := goquery.NewDocumentFromReader(resp.Body) //#content > div > div.article > ol > li:nth-child(1) //#content > div > div.article > ol > li:nth-child(2) doc.Find("#content > div > div.article > ol > li").Each(func(i int, selection *goquery.Selection) { title := selection.Find("div > div.info > div.hd > a > span:nth-child(1)").Text() img := selection.Find("div > div.pic > a > img") imgUrl, ok := img.Attr("src") quote := selection.Find("div > div.info > div.bd > p.quote > span").Text() score := selection.Find("div > div.info > div.bd > div > span.rating_num").Text() info := selection.Find("div > div.info > div.bd > p:nth-child(1)").Text() if ok { year, director, actor := InfoHandler(info) saveToDb(title, imgUrl, year, score, quote, director, actor) } }) } func saveToDb(title, imgUrl, year, score, quote, director, actor string) { err := engine.Sync(new(MovieData)) if err != nil { fmt.Println("保存数据出现错误") } moviedata := MovieData{ Title: title, Year: year, Score: score, Director: director, Actor: actor, Quote: quote, Picture: imgUrl, } affected, err := engine.Insert(&moviedata) if err != nil { fmt.Println("插入出现错误!") } fmt.Println("保存:", strconv.FormatInt(affected, 10)) } func InfoHandler(info string) (year, director, actor string) { year_re, _ := regexp.Compile(`(\d+)`) year = string(year_re.Find([]byte(info))) director_re, _ := regexp.Compile(`导演:(.*?)\s*(主演:|$)`) director_result := director_re.FindStringSubmatch(info) if len(director_result) > 1 { director = director_result[1] } actor_re, _ := regexp.Compile(`主演:(.*)`) actor_result := actor_re.FindStringSubmatch(info) if len(actor_result) > 1 { actor = actor_result[1] } return year, director, actor } 爬B站评论 Go语言爬虫还是挺累人，它在解析json数据要使用结构体的形式，这里可以使用json2struct.mervine.net网站来转换成结构体，并提取出评论部分。 package main import ( "fmt" "github.com/goccy/go-json" "io" "log" "net/http" ) type KingRankResp struct { Code int64 `json:"code"` Data struct { Replies []struct { Content struct { Device string `json:"device"` JumpURL struct{} `json:"jump_url"` MaxLine int64 `json:"max_line"` Members []interface{} `json:"members"` Message string `json:"message"` Plat int64 `json:"plat"` } `json:"content"` Count int64 `json:"count"` Folder struct { HasFolded bool `json:"has_folded"` IsFolded bool `json:"is_folded"` Rule string `json:"rule"` } `json:"folder"` Like int64 `json:"like"` Replies []struct { Action int64 `json:"action"` Assist int64 `json:"assist"` Attr int64 `json:"attr"` Content struct { Device string `json:"device"` JumpURL struct{} `json:"jump_url"` MaxLine int64 `json:"max_line"` Message string `json:"message"` Plat int64 `json:"plat"` } `json:"content"` Rcount int64 `json:"rcount"` Replies interface{} `json:"replies"` } `json:"replies"` Type int64 `json:"type"` } `json:"replies"` } `json:"data"` Message string `json:"message"` } func main() { client := &http.Client{} req, err := http.NewRequest("GET", "https://api.bilibili.com/x/v2/reply/wbi/main?oid=338159898&type=1&mode=3&pagination_str={\"offset\":\"\"}&plat=1&seek_rpid=&web_location=1315875&w_rid=1b7a0abc9704055facaafb28b36d864e&wts=1704203337", nil) if err != nil { log.Fatal(err) } req.Header.Set("authority", "api.bilibili.com") req.Header.Set("sec-ch-ua-mobile", "?0") req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.93 Safari/537.36") req.Header.Set("accept", "*/*") req.Header.Set("accept-language", "zh-CN,zh;q=0.9") resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() bodyText, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } var resultList KingRankResp _ = json.Unmarshal(bodyText, &resultList) for _, result := range resultList.Data.Replies { fmt.Println("一级评论：", result.Content.Message) for _, reply := range result.Replies { fmt.Println("二级评论：", reply.Content.Message) } } } Gin Gin框架与flask十分的相似，都是路由、处理函数对应，感觉都十分的轻量化。 响应 启动方式 Gin框架的启动方式库有分为两种： 使用Run()函数启动，实际上也是第二种的一个封装 使用原生的http.ListenAndServe()启动 响应JSON、XML、yaml、html 这里对于响应的文件格式方式，都是几乎一致的，只需要使用context.JSON()、contenxt.XML()等换一个函数的形式返回即可。至于返回的数据，一般又三种方式： 使用struct包装json等形式返回，注意字段开头要大写才能进行json序列化解析 使用map包装json等形式返回 使用gin.H{}直接返回数据 至于HTML，只需要通过LoadHTMLGlob()函数指定存放的HTML文件的目录，随后通过context.HTML()指定返回的文件和数据即可在前端完成渲染。 响应静态文件 响应静态文件一般使用以下方式： 对应文件的路径，没有相对路径，只有基于项目的路径，并且两种静态文件的URL前缀是不可重复的。 StaticFile() StaticFile() 方法用于指定单个文件的静态文件服务。 语法：router.StaticFile("/path", "filepath") StaticFS() StaticFS() 方法用于指定整个文件系统的静态文件服务。 语法：router.StaticFS("/path", http.Dir("rootpath")) 重定向 重定向也是直接使用contenxt.Redirect()函数重定向来地址即可，但是要注意301和302重定向的不同： 301表示永久重定向，强缓存，会将响应的结果缓存起来 302是临时重定向，是不会进行缓存的。 查询参数 静态参数 query()获取URL中的参数，如果没有返回空 GetQuery()获取URL的参数key的值，没有返回false QueryArray()可以拿到多个相同的查询参数 动态参数 动态参数也就是可变的参数，比如说/example/1中的1可通过改变显示不同的查询数据，可通过/:key的形式定义查询的动态参数路径。 表单参数 PostFormArray()可获取多个表单参数，返回数组的形式 PostForm()获取表单参数，获取不到返回空，可以接受form-data和x-www-form-urlencoded两种形式 DefaultForm()假如没有接受到这个参数，可设置默认的值 MultipartForm()接受所有的post参数，包括文件等，返回Map的形式。 原始参数 GetRawData()可以获取原始参数，如果是x-www-form-urlencoded返回key=value的ASCII码值，如果是form-data返回Content-Dispostion的那种形式。 demo: package main import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" "net/http" ) func Index(context *gin.Context) { context.String(200, "Hello World") } func Json(context *gin.Context) { //开头字母一定要大写才能使Json序列化可见 type User struct { Username string Nickname string Password string `json:"-"` //json不进行响应 } userInfo := User{Username: "Aiwin", Nickname: "Lau", Password: "123456"} context.JSON(200, userInfo) //map响应 /*userMap := map[string]string{ "username": "Aiwin", "nickename": "Lau", } context.JSON(200, userMap)*/ //直接响应json //context.JSON(200, gin.H{"username": "aiwin", "nickename": "Lau"}) } // Html 响应HTML页面 func Html(context *gin.Context) { context.HTML(200, "index.html", gin.H{"username": "Aiwin"}) } func redirect(context *gin.Context) { //永久重定向,强缓存,将响应结果缓存起来 context.Redirect(301, "http://www.aiwin.net.cn") //临时重定向,不会进行缓存 context.Redirect(302, "http://www.aiwin.net.cn") } // 获取静态查询参数 func query(context *gin.Context) { fmt.Println(context.GetQuery("user")) fmt.Println(context.GetQueryArray("user")) } // 获取动态路径参数 func param(context *gin.Context) { fmt.Println(context.Param("user_id")) } // form表单 func form(context *gin.Context) { fmt.Println(context.PostForm("name")) fmt.Println(context.PostFormArray("name")) fmt.Println(context.DefaultPostForm("age", "18")) } func raw(context *gin.Context) { body, _ := context.GetRawData() contentType := context.GetHeader("Content-Type") //解析json数据 switch contentType { case "application/json": type User struct { Username string `json:"username"` Password string `json:"password"` } var user User err := json.Unmarshal(body, &user) if err != nil { fmt.Println(err) } fmt.Println(user) } } func main() { Engine := gin.Default() Engine.LoadHTMLGlob("网络编程/gin/templates/*") //GoLang中只有相对项目路径,响应下载文件 Engine.StaticFile("/static/demo", "网络编程/gin/static/demo.jpg") //注意前缀不要重复,否则会报错 Engine.StaticFS("/fsfile", http.Dir("网络编程/gin/static")) //定于路由和对应的处理函数 Engine.GET("/index", Index) Engine.GET("/json", Json) Engine.GET("/html", Html) Engine.GET("/redirect", redirect) Engine.GET("/query", query) Engine.GET("/param/:user_id", param) Engine.POST("/form", form) Engine.POST("/raw", raw) //第一种启动方式 Engine.Run("127.0.0.1:8000") //第二种启动方式,用原生的http服务器,Engine.Run只是原生http的封装 //http.ListenAndServe("127.0.0.1:5000", Engine) } 四大请求方式 RESTful 风格是一种用于设计和构建网络应用程序的软件架构风格。它基于 HTTP 协议，并遵循一些设计原则和约束，旨在提供简单、可扩展、可靠和可轻松集成的 Web 服务。 GET:从服务器取除资源 POST在服务器中新建一个资源 PUT在服务器中更新资源 PATCH在服务器中更新资源 DELETE从服务器中删除资源 简单demo演示： GET 文章列表 GET /article/:id 文章详情 POST 添加文章 PUT /article/:id 更新文章 DELETE 删除文章 package main import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" "strconv" ) type article struct { Title string `json:"title"` Detail string `json:"detail"` } //模拟数据库 var articlelist = []article{ {"西游记", "《九九八十一难》"}, {"水浒传", "《梁山一百零八好汉》"}, {"三国演义", "《三国鼎立传奇故事》"}, } type response struct { Code int `json:"code"` Data any `json:"data"` Msg string `json:"msg"` } func _bindJson(context *gin.Context, obj any) (err error) { body, _ := context.GetRawData() contentType := context.GetHeader("Content-Type") switch contentType { case "application/json": err = json.Unmarshal(body, &obj) if err != nil { fmt.Println(err.Error()) return err } } return nil } func getlist(context *gin.Context) { context.JSON(200, response{Code: 0, Data: articlelist, Msg: "成功"}) } func getDetail(context *gin.Context) { id, _ := strconv.Atoi(context.Param("id")) context.JSON(200, response{Code: 0, Data: articlelist[id], Msg: "成功"}) } func create(context *gin.Context) { var newarticle article err := _bindJson(context, &newarticle) if err != nil { fmt.Println(err) } articlelist = append(articlelist, newarticle) context.JSON(200, response{Code: 0, Data: nil, Msg: "添加成功"}) } func edit(context *gin.Context) { id, _ := strconv.Atoi(context.Param("id")) var editarticle article err := _bindJson(context, &editarticle) if err != nil { fmt.Println(err) } articlelist[id].Title = editarticle.Title articlelist[id].Detail = editarticle.Detail context.JSON(200, response{Code: 0, Data: editarticle, Msg: "修改成功"}) } func delete(context *gin.Context) { id, _ := strconv.Atoi(context.Param("id")) articlelist = append(articlelist[:id], articlelist[id+1:]...) context.JSON(200, response{Code: 0, Data: articlelist, Msg: "删除成功"}) } func main() { router := gin.Default() router.GET("/getlist", getlist) router.GET("/getDetail/:id", getDetail) router.POST("/create", create) router.PUT("/edit/:id", edit) router.DELETE("/delete/:id", delete) router.Run("0.0.0.0:8888") } 请求头相关 通过getHeader()获取请求头，不区分大小写，使用-来进行连接，通过Request.Header可获取全部请求头的map，但是要注意Request.Header如果用Map取值方式是必须大小写一致的(因为是从Map 中获取value的）。 package main import ( "fmt" "github.com/gin-gonic/gin" ) func index(context *gin.Context) { fmt.Println(context.GetHeader("user-agent")) fmt.Println(context.Request.Header.Get("user-agent")) fmt.Println(context.Request.Header["User-Agent"]) context.Header("token", "Hello-World") context.Header("Content-Type", "application/text;charset=utf-8") context.JSON(0, gin.H{"data": "自定义响应头"}) } func main() { router := gin.Default() router.GET("/index", index) router.Run("0.0.0.0:8080") } 绑定参数 gin中可以使用bind快速将前端传递来的数据与结构体进行参数绑定，以及参数校验。 ShouldBind：在绑定过程中如果出现错误，将错误信息返回给调用者，由调用者决定如何处理错误。 MustBind：在绑定过程中如果出现错误，将直接触发panic，使程序进入恐慌状态并终止运行。 package main import "github.com/gin-gonic/gin" // form指示可以接受form表单 // uri表示可以通过uri query查询 type User struct { Name string `json:"name" form:"name" uri:"name"` Age int `json:"age" form:"age" uri:"age"` Sex string `json:"sex" form:"sex" uri:"sex"` } func json(context *gin.Context) { var user User err := context.ShouldBindJSON(&user) if err != nil { context.JSON(200, gin.H{"msg": "出错了"}) return } context.JSON(200, user) } func uri(context *gin.Context) { var user User err := context.ShouldBindUri(&user) if err != nil { context.JSON(200, gin.H{"msg": "出错了"}) return } context.JSON(200, user) } func form(context *gin.Context) { var user User err := context.ShouldBind(&user) if err != nil { context.JSON(200, gin.H{"msg": "出错了"}) return } context.JSON(200, user) } func main() { router := gin.Default() router.GET("/json", json) router.POST("/form", form) router.GET("/uri/:name/:age/:sex", uri) router.Run("0.0.0.0:8888") } 常用验证器 required：字段必需存在且不能为空。例如，required:"true"。 min：字段的值必须大于或等于指定的最小值。可以用于数字，字符串和时间类型的验证。例如，min:"10"。 max：字段的值必须小于或等于指定的最大值。同样适用于数字，字符串和时间类型的验证。例如，max:"100"。 len：字段的长度必须等于指定的值。例如，len:"10"。 email：字段的值必须为有效的电子邮件地址。例如，email:"true"。 url：字段的值必须为有效的URL地址。例如，url:"true"。 regex：字段的值必须符合指定的正则表达式。例如，regex:"^[A-Za-z]+$"。 alpha：字段的值必须全部由字母组成，不允许包含空格、数字或特殊字符。例如，alpha:"true"。 alphanum：字段的值必须全部由字母和数字组成，不允许包含空格或特殊字符。例如，alphanum:"true"。 numeric：字段的值必须为数字型的数据，可以是整数或浮点数。例如，numeric:"true"。 oneof：字段的值必须属于提供的多个值中的一个。例如，oneof:"dog cat bird"。 gte：字段的值必须大于或等于指定的另一个字段的值。例如，gte:"Age"。 lte：字段的值必顶小于或等于指定的另一个字段的值。例如，lte:"EndTime"。 eqfield，nefield等于和不等于其它字段的值 contains 一定要包含的字符串 excludes 不包含的字符串 startswith 字符串前缀 endwith 字符串后缀 ip ipv4 ipv6 uri url 特定的格式 datetime 日期验证，如datetime=2002-03-04 15:00:01等表示日期验证 dive 针对数组 package main import "github.com/gin-gonic/gin" type Member struct { Username string `json:"username" binding:"required,min=5,max=10"` Password string `json:"password" binding:"required" ` Re_password string `json:"re_password" binding:"eqfield=Password"` } func verifyJson(context *gin.Context) { var member Member err := context.ShouldBindJSON(&member) if err != nil { context.JSON(200, gin.H{"msg": err.Error()}) return } context.JSON(200, member) } func main() { router := gin.Default() router.GET("/verifyJson", verifyJson) router.Run("0.0.0.0:8888") } 自定义错误信息 package main import ( "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "reflect" ) type MemberInfo struct { Username string `json:"username" binding:"required,min=5,max=10" msg:"用户名长度不符合"` Password string `json:"password" binding:"required" msg:"密码不能为空"` Re_password string `json:"re_password" binding:"eqfield=Password" msg:"两次密码必须一致"` } func GetValidMsg(err error, obj any) string { //obj是结构体指针 getObj := reflect.TypeOf(obj) //将err接口断言为是否为验证器返回的错误 if errs, ok := err.(validator.ValidationErrors); ok { //循环每一个错误信息 for _, e := range errs { //根据报错字段拿到自定义错误信息 if f, exist := getObj.Elem().FieldByName(e.Field()); exist { return f.Tag.Get("msg") } } } return err.Error() } func Mymessage(context *gin.Context) { var member MemberInfo err := context.ShouldBindJSON(&member) if err != nil { context.JSON(200, gin.H{"msg": GetValidMsg(err, &member)}) return } context.JSON(200, member) } func main() { router := gin.Default() router.GET("/verifyJson", Mymessage) router.Run("0.0.0.0:8888") } 自定义验证器 package main import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "reflect" ) type Student struct { Name string `json:"name" binding:"required,sign" msg:"用户名校验失败"` Age int `json:"age" binding:"required"` } func _GetValidMsg(err error, obj any) string { getObj := reflect.TypeOf(obj) if errs, ok := err.(validator.ValidationErrors); ok { for _, e := range errs { if f, exist := getObj.Elem().FieldByName(e.Field()); exist { return f.Tag.Get("msg") } } } return err.Error() } func verify(context *gin.Context) { var student Student err := context.ShouldBindJSON(&student) if err != nil { context.JSON(200, gin.H{"msg": _GetValidMsg(err, &student)}) return } context.JSON(200, student) } func signValid(fl validator.FieldLevel) bool { var namelist = []string{"admin"} for _, namestr := range namelist { name := fl.Field().Interface().(string) if name == namestr { return false } } return true } func main() { Engine := gin.Default() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { //自定义验证器 v.RegisterValidation("sign", signValid) } Engine.POST("/verify", verify) Engine.Run("0.0.0.0:8000") } 文件上传 saveUploadFile()直接保存文件到服务器 ，本质上还是Create+Copy Ceate+Copy 可以直接通过Create创建文件，通过Copy保存Open()出来的内容 package main import ( "fmt" "github.com/gin-gonic/gin" "io" "os" ) func upload(context *gin.Context) { files, err := context.FormFile("file") if err != nil { context.String(500, "上传失败") return } fileInterface, _ := files.Open() //通过SaveUploadedFile()进行处理, 本质上还是Create+Copy //err = context.SaveUploadedFile(files, "网络编程/文件上传/uploads/"+files.Filename) //if err != nil { // context.JSON(500, "上传失败") // return //} fmt.Println(files.Filename) result, _ := os.Create("网络编程/文件上传/uploads/" + files.Filename) defer result.Close() io.Copy(result, fileInterface) context.String(200, "上传成功") } func uploads(context *gin.Context) { form, err := context.MultipartForm() if err != nil { context.String(500, "上传错误") return } files := form.File["file[]"] for _, file := range files { context.SaveUploadedFile(file, "网络编程/文件上传/uploads/"+file.Filename) } context.String(200, "上传成功") } func main() { Engine := gin.Default() Engine.POST("/upload", upload) Engine.POST("/uploads", uploads) Engine.Run("0.0.0.0:8000") } 文件下载 通过Header()设置文件的方式，唤起浏览器进行下载。 func download(context *gin.Context) { context.Header("Content-Type", "application/octet-stream") context.Header("Content-Disposition", "attachment; filename=download.jpg") context.File("网络编程/文件上传/uploads/5.jpg") } 中间件 Gin框架允许开发者在处理请求的过程中，假如自己的函数也就是中间件，一般在函数处理的前面，可用于登录认证，权限校验，数据分页等等。 在定义路由的时候，其实可以定义多个函数按顺序进行处理，路由对应的处理函数其实也是一个中间件，但是一般处理路由不会进行验证拦截，而我们可以在真正的处理函数前面定义其它函数并进行一些Abort()验证拦截操作，当然也可以通过Next()指定下一个要走的中间件，如果后面没有中间件，则会走回去第一个中间件。一般用的比较多的是全局中间件。 这里记录下gin.Default()和gin.New()的配置: gin.Default() 方法会返回一个默认的 Gin 引擎实例，并且会自动注入 Logger 和 Recovery 中间件，同时也会使用默认的配置，包括日志输出到控制台等。 gin.New() 方法则会返回一个裸露的 Gin 引擎实例，不会自动注入任何中间件或默认配置，需要手动进行配置。 package main import "github.com/gin-gonic/gin" func middleware(context *gin.Context) { username, ok := context.GetQuery("username") if ok { if username == "admin" { context.Set("name", "admin") //中间件传递值 context.Next() } else { context.JSON(200, gin.H{"msg": "你不是admin"}) context.Abort() } } } func main() { router := gin.Default() router.Use(middleware) router.GET("/login", func(context *gin.Context) { name, _ := context.Get("name") context.JSON(200, gin.H{"msg": "欢迎" + name.(string) + "用户"}) }) router.Run("0.0.0.0:8000") } 路由分组 就是给路由匹配一些前缀路由，避免重复书写，比较方便，也比较美观。 package main import "github.com/gin-gonic/gin" type User struct { Name string `json:"name"` Age int `json:"age"` } type Article struct { Title string `json:"title"` Content string `json:"content"` } type Response struct { Code int `json:"code"` Data any `json:"data"` Msg string `json:"msg"` } func Usermiddleware(ctx *gin.Context) { token := ctx.GetHeader("token") if token == "admin" { ctx.Next() } else { ctx.JSON(403, gin.H{"msg": "未通过验证"}) ctx.Abort() } } func UserListView(ctx *gin.Context) { var userList = []User{ {"Aiwin", 20}, {"zhangsan", 21}, {"lisi", 22}, } ctx.JSON(200, Response{0, userList, "请求成功"}) } func getArticleList(ctx *gin.Context) { var articleList = []Article{ {"《变形记》", "讲变形的故事"}, {"《西游记》", "讲取经的故事"}, {"《小王子》", "讲王子的故事"}, } ctx.JSON(200, Response{0, articleList, "请求成功"}) } func main() { router := gin.Default() api := router.Group("api") //定义路由分组 api.GET("/getUserList", UserListView).Use(Usermiddleware) // /api/getUserList article := api.Group("article") //双重路由 article.GET("/getArticleList", getArticleList) ///api/article/getUserList router.Run("0.0.0.0:8000") } 日志 记录用户操作，猜测用户行为 记录bug 通过gin.DefaultWriter()来指定一个os.Create()的file，能够利用内置的日志器。 gin.DebugPrintRouteFunc()用于打印当前注册的所有路由信息，可以使用Route()函数打印所有的路由更快。 如果不想看debug信息，可以通过gin.SetMode()切换环境 package main import ( "fmt" "github.com/gin-gonic/gin" "io" "os" ) func LogFormatterParams(params gin.LogFormatterParams) string { return fmt.Sprintf( "[Aiwin] %s | %s%d%s | %s %s %s %s \n", params.TimeStamp.Format("2024-01-16 15:00:00"), params.StatusCodeColor(), params.StatusCode, params.ResetColor(), params.MethodColor(), params.Method, params.ResetColor(), params.Path) } func main() { gin.SetMode(gin.ReleaseMode) file, _ := os.Create("log.txt") gin.DefaultWriter = io.MultiWriter(file, os.Stdout) router := gin.Default() //自定义打印日志的格式 //router := gin.New() //router.Use(gin.LoggerWithFormatter(LogFormatterParams); //router.Use(gin.LoggerWithConfig(gin.LoggerConfig{Formatter: LogFormatterParams})) for _, tree := range router.Routes() { fmt.Println(tree.Method, tree.Path, tree.HandlerFunc) } router.GET("/index", func(context *gin.Context) {}) router.Run("0.0.0.0:8000") } logrus go get github.com/sirupsen/logrus 一些常用的命令，少于Info等级就不会打印出来，可以通过logrus.SetLevel()更改等级： logrus.Error()错误信息 logrus.Warnln()警告信息 logrus.Infof()普通信息 logrus.Debug()调试信息 logrus.Println()打印信息 logrus.SetFormatter()一些字段配置 名称 作用 ForceColors 是否强制使用颜色输出 DisableColors 是否禁用颜色输出 ForceQuote 是否强制引用所有值 DisableQuote 是否禁止引用所有值 DisableTimestamp 是否禁用时间戳记录 FullTimestamp 是否连接到TTY时输出完整的时间戳 TimestampFormat 用于输出完整时间戳的时间戳格式 一些颜色的输出如下： fmt.Println("\033[30m 黑色 \033[0m") fmt.Println("\033[31m 红色 \033[0m") fmt.Println("\033[32m 绿色 \033[0m") fmt.Println("\033[33m 黄色 \033[0m") fmt.Println("\033[34m 蓝色 \033[0m") fmt.Println("\033[35m 紫色 \033[0m") fmt.Println("\033[36m 青色 \033[0m") fmt.Println("\033[37m 灰色 \033[0m") // 背景色 fmt.Println("\033[40m 黑色 \033[0m") fmt.Println("\033[41m 红色 \033[0m") fmt.Println("\033[42m 绿色 \033[0m") fmt.Println("\033[43m 黄色 \033[0m") fmt.Println("\033[44m 蓝色 \033[0m") fmt.Println("\033[45m 紫色 \033[0m") fmt.Println("\033[46m 青色 \033[0m") fmt.Println("\033[47m 灰色 \033[0m") 自定义logrus输出格式: package main import ( "bytes" "fmt" "github.com/sirupsen/logrus" "os" "path" ) const ( red = 31 yellow = 33 blue = 36 gray = 37 ) type LogFormatter struct{} func (t *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) { var levelColor int switch entry.Level { case logrus.DebugLevel, logrus.TraceLevel: levelColor = gray case logrus.WarnLevel: levelColor = yellow case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel: levelColor = red default: levelColor = blue } var b *bytes.Buffer if entry.Buffer != nil { b = entry.Buffer } else { b = &bytes.Buffer{} } timestamp := entry.Time.Format("2006-01-02 15:04:05") if entry.HasCaller() { //自定义文件路径 funcVal := entry.Caller.Function fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line) //自定义输出格式 fmt.Fprintf(b, "[%s] \x1b[%dm[%s]\x1b[0m %s %s %s\n", timestamp, levelColor, entry.Level, fileVal, funcVal, entry.Message) } else { fmt.Fprintf(b, "[%s] \x1b[%dm[%s]\x1b[0m %s\n", timestamp, levelColor, entry.Level, entry.Message) } return b.Bytes(), nil } var log *logrus.Logger func init() { log = NewLog() } func NewLog() *logrus.Logger { mLog := logrus.New() //新建一个实例 mLog.SetOutput(os.Stdout) //设置输出类型 mLog.SetReportCaller(true) //开启返回函数名和行号 mLog.SetFormatter(&LogFormatter{}) //设置自己定义的Formatter mLog.SetLevel(logrus.DebugLevel) //设置最低的Level return mLog } func main() { log.Errorln("你好") log.Infof("你好") log.Warnln("你好") log.Debugf("你好") }
一、什么是Node.js? 基于Chrome V8引擎的Javascript的运行环境，V8相对于火狐的OdinMonkey，Safri浏览器的JSCore都要快。 Node.js是Javascript的后端运行环境，无法调用DOM和BOM等浏览器内置API，仅仅提供了基础的功能和API，这些基础使很多强大的框架出现，可以基于Express框架构建Web应用，基于restify快速构建API项目，可以读写和操作数据库等。 二、fs文件系统模块 fs模块是Node.js官方提供的用来操作文件的模块，提供一系列的方法和属性，用来满足用户对文件的操作，有如下的一些常用命令。 命令 说明 open(path，mode，callback) 异步模式下打开文件 stat(path，callback) 异步模式下获取文件信息 writeFile(file，data，callback) 异步模式下写入文件 read(fd，buffer，offset，length，position，callback) 通过文件描述符读取文件 close(fd，callback) 通过文件描述符读取文件 ftruncate(fd，len，callback) 通过文件描述符读取文件 fs.unlink(path, callback) 删除文件 fs.mkdir(path[, options], callback) 创建目录 fs.readdir(path, callback) 读取目录 fs.rmdir(path, callback) 删除目录 fs.exists(path, callback) 文件是否存在 例子： const fs=require('fs') const buf = new Buffer.alloc(1024); fs.mkdir('../file/',function (err) { if(err){ console.log(err) } console.log("目录创建成功") }) fs.writeFile('../file/input.txt','通过writeFile写入文件内容',function (err) { if(err){ return console.log(err) } console.log('文件写入成功') }) fs.readdir("../file/",function (err,files) { if(err){ return console.error(err) } files.forEach(function (file) { console.log("目录读取开始") console.log(file) }) }) fs.open('../file/input.txt','r+',function (err,fd) { if(err){ return console.error(err) } console.log("开始文件读取") fs.read(fd,buf,0,buf.length,0,function (err,bytes) { if(err){ console.log(err) } if(bytes>0){ console.log(buf.subarray(0,bytes).toString()) } console.log(bytes+"字节被读取") }) fs.close(fd,function (err) { if(err){ console.log(err) } console.log("文件读取关闭") }) }) fs.unlink("../file/input.txt",function (err) { if(err){ return console.error(err) } console.log("文件删除成功") }) setTimeout(function() { fs.rmdir("../file/", function (err) { if (err) { return console.log(err) } console.log("目录删除成功") }) },3000) path路径模块 Node在读取或者写入文件，使用相对路径的时候，会以Node使用的目录为起点，直接拼接相对目录的字符串，可以使用绝对路径或者path模块解决。 命令 说明 sep 平台路径分割符 join() 拼接路径 basename() 最后的文件名 extname() 文件扩展名 isAbsolute() 判断是否为绝对路径 relative 转为相对路径 normalize 规范化路径 const path=require('path') console.log(path.sep) //路径分割符 const pathStr=path.join("/tmp","/Node","fs","../","path") console.log(pathStr) const pathStr1=path.join(__dirname,'../files/input.txt') console.log(pathStr1) console.log(path.basename("../files/input.txt")) //获取最后的文件名 console.log(path.extname("../files/input.txt")) //文件扩展名 console.log(path.isAbsolute("/tmp/node/path")) //判断是否是绝对路径 console.log(path.relative("/tmp/node/path","/tmp/node/http/")) //转换成相对路径 console.log(path.normalize('/test/test1//2slashes/1slash/tab/../')); //规范化路径 烟花分割案例 在index.html页面中，CSS和Javascript都写入了里面，没有分成index.css，index.js文件，使用fs和path对index.html分割成这几个文件。 原index.html: 烟花动画特效 const fs=require('fs') const path=require('path') const regStyle=/ ","") fs.writeFile(path.join(__dirname,"static/style.css"),newCss,function (err) { if(err) { return console.log("CSS文件写入失败"+err.message) } }) } function resoleJs(htmlStr){ const r=regJs.exec(htmlStr) const newJs=r[0].replace(" ","") fs.writeFile(path.join(__dirname,"static/index.js"),newJs,function (err) { if(err) { return console.log("JS文件写入失败"+err.message) } }) } function resoleHtml(htmlStr) { const r=regStyle.exec(htmlStr) const r1=regJs.exec(htmlStr) const pathStyle="./style.css" const pathStyle1="./index.js" const newHtml=htmlStr.toString().replace(r[0],' ').replace(r1[0],' ') fs.writeFile(path.join(__dirname,"static/index.html"),newHtml,function (err) { if(err){ return console.log("文件写入失败"+err.message) } }) } 三、Http模块 http模块是Node.js官方提供的，用来创建web服务器的模块，通过http模块提供的createServer()方法，将电脑变成Web服务器，对外提供Web资源服务。 烟花案例 const http=require('http') const fs=require('fs') const url = require("url") const path=require('path') server=http.createServer() server.on('request' ,function (req,res) { const url = req.url let pathname='' if(url==='/' || url==='/index.html'){ pathname=path.join(__dirname,'./static/index.html') } else{ pathname=path.join(__dirname,'./static',url) } fs.readFile(pathname,'utf-8',function (err,data) { if(err){ res.write("出现未知错误"+err.message) } res.end(data) }) }) server.listen(8080,function () { console.log("服务器运行在http://127.0.0.1:8080") }) Get请求 const http = require('http'); const url = require('url'); http.createServer(function(req, res){ res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'}); const params = url.parse(req.url, true).query; if(params.username==="admin"&&params.password==="admin123"){ res.write(" 登录成功 ") } res.end(); }).listen(8080); post请求 const http=require("http") const querystring=require("querystring") let postHTML=' Node.js 例子 ' + ' ' + ' ' + '用户名： ' + '密码： ' + ' ' + ' ' + ' '; server=http.createServer(function (req,res) { let body=""; req.on('data',function (chunk) { body+=chunk }) req.on('end',function () { body=querystring.parse(body) res.writeHead(200, {'Content-Type': 'text/html; charset=utf8'}); if(body.username==="admin"&&body.password==="admin123"){ res.write(" 登录成功 ") } else{ res.write(postHTML) } res.end() }) }).listen(8080) 四、模块化 模块化在编程中指的是遵守固定规则，把一个大文件拆成相互依赖的多个小模块，从而提高代码的复用性，可维护性，按需加载等。 模块化规范：对代码进行模块化拆分和组合时，要遵守的规则，从而方便了各大模块之间的相互调用。 Node.js的模块分类：内置模块(官方提供），自定义模块(用户自己创建)，第三方模块(第三方开发，使用前下载） 模块作用域：为了防止全局变量污染，自定义模块中定义的变量和方法等成员，只能在当前模块被访问。 向外共享模块作用域成员：每个.js自定义模块中都有module对象，存储了和当前模块的有关信息，使得模块内成员可以通过export被模块外访问到。 向外共享成员例子： //向module.exports对象上挂载username属性或方法 module.exports.username='Aiwin' module.exports.say=function () { console.log("您好,exports向外共享") } function NoInvoke() { console.log("这里不能暴露出去") } const m=require("./test") console.log(m) 使用require()方法导入模块时，导入的结果永远以module.exports指向的新对象为准，即exports对象可以被重覆盖。 //向module.exports对象上挂载username属性或方法 module.exports.username='Aiwin' module.exports.say=function () { console.log("您好,exports向外共享") } //require指向永远以新的对象为准 module.exports={ name:'Aiwin', say(){ console.log("Hello") } } function NoInvoke() { console.log("这里不能暴露出去") } 为了简化向外共享成员的代码，Node提供了exports对象，默认情况下，module.exports和exports指向同一对象，最终共享的结果以module.exports指向对象为准。 npm和包：NodeJs中的包就是由第三方个人或团队开发出来的模块，免费供所有人使用，包基于内置模块封装出来，提供了更高级，更方便的API，极大的提高了开发效率。 全球最大的包共享平台 npm初体验 传统初始化时间 function dataFormat(timeStr){ const data=new Date(timeStr) const y=data.getFullYear() const m=padZero(data.getMonth()) const d=padZero(data.getDate()) const hh=padZero(data.getHours()) const mm=padZero(data.getMinutes()) const ss=padZero(data.getSeconds()) return `${y}-${m}-${d} ${hh}:${mm}:${ss}` } function padZero(n) { return n>9?n:'0'+n } module.exports={ dataFormat } const time=require("./dataFormat") const t=new Date() console.log(t) console.log(time.dataFormat(t)) npm的一些使用： 命令 说明 npm install 包完整名称@版本号 安装npm包 npm i 包名称@版本号 安装npm包 npm init -y 创建package.json文件 npm config get registry 查看镜像源 npm config set registry=https://registry.npm.taobao.org/ 安装淘宝镜像源 npm i nrm -g 下载nrm npm uninstall 包名 -g 卸载全局安装的包 nrm ls 查看可下载镜像源 nrm use 名称 更换镜像源 npm uninstall 卸载包 npm i 包名 -D 将包记录到devDependencies节点 npm i / npm install 自动从dependencies中识别包下载 devDependencies中的包是指项目只在开发阶段用到的包，项目上线后遇不到，dependencies中有项目安装的npm包。 使用npm const moment=require('moment') const time=moment().format('YYYY-MM-DD HH:mm:ss') console.log(time) 规范的包结构，应包含以下3点要求： （1）包必须以单独的目录存在。 （2）包的顶级目录必须包含package.json管理配置文件。 （3）package.json必须包含name、version、main这三个属性，代表包名、版本、入口。 五、开发属于自己的包 功能要求：1.转义HTML中的特殊字符 2.还原转义字符 步骤如下：1. 创建新目录作为包的根目录 2.在根目录下创建入口js，package.json，README.MD 3.编写package.json和代码 注意包名必须唯一，创建包名前须查看是否存在相同包名 创建package.json package.json的一些基本信息，包名，版本号，入口，关键词，协议 编写index.js代码 function htmlEscape(html) { return html.replace(/<|>|"|&/g,match=>{ switch (match) { case "<": return "<" case ">": return ">" case '"': return """ case "&": return "&" } }) } function htmlUnEscape(html) { return html.replace(/<|>|"|&/g,match=>{ switch (match) { case "<": return "<" case ">": return ">" case '"': return '"' case "&": return "&" } }) } module.exports={ htmlEscape, htmlUnEscape } 测试： const {htmlEscape, htmlUnEscape} = require("../Aiwin-tools"); const htmlStr=' 这是一次测试Test ' const str=htmlEscape(htmlStr) console.log(str) console.log(htmlUnEscape(str)) 编写README.MD文档 将包用法说清楚即可，没有特别的规范 将包发布到npm 前往npm官网注册一个账号 npm login登录npm账号,注意登录服务器必须为官网的服务器 npm publish 发布包 npm unpublish 包名 --force 删除发布72小时内发布的包 模块加载机制 模块加载机制，优先从缓存中加载，即多次调用require()不会导致模块代码被执行多次。 内置模块加载机制，内置模块的加载优先级最高，如require('fs')始终返回内置的fs模块，即可node_modules目录下有相同的包叫fs。 自定义模块加载机制，使用require()加载自定义模块，必须指定./或../开头的路径标识符，如果省略文件名，则会按照顺序为确切文件名，补.js，.json，.node扩展名加载。 第三方模块加载机制，如果传递给require()的模块标识符不是内置模块，也没有./或者../开头，则会从当前目录向上寻找node_modules文件夹加载第三方模块。 目录作为模块标识符，会在被加载目录下寻找package.json文件，寻找main属性作为加载入口，若没有，则加载目录下index.js文件，若两者都没有，则报错。 六、Express 1.初识Express Express基于Node.js平台，快读、开放、极简的Web开发框架，通俗的说就是http模块的加强版，是基于http模块进一步封装出来，极大提高开发效率，专门用于创建Web服务器。 安装express安装到项目中，直接npm install下载即可。 创建基本Web服务器 const express = require('express'); const app = express(); app.get('/', function (req, res) { res.send('Hello express'); }) const server = app.listen(8081, function () { const host = server.address().address; const port = server.address().port; console.log("express server running at https://%s:%s", host, port) }); GET/POST const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.urlencoded({ extends: false })) app.use('./public',express.static('public')) app.get('/', function (req, res) { res.sendFile( __dirname + "/public/" + "index.html" ); }) app.post('/post',function (req,res) { let response={ "username":req.body.username, "password":req.body.password } res.end(JSON.stringify(response)) }) //动态参数 app.get('/user/:username/:password',function (req,res) { res.send(req.params) }) const server = app.listen(8081, function () { const {address,port} = server.address(); console.log("express server running at https://%s:%s", address, port) }); Express Username: Password: express.static()创建静态资源服务器，如果有多个静态资源目录，会按添加顺序查找所需文件，注意post请求中的body要解析才能提取参数。 nodemon 用于在编写调试Node.js项目时，修改了项目代码，工具会自动监听项目文件变动，当代码被修改后，自动重启项目，方便开发和调试，类似于springboot的devtools。 安装，npm install -g nodemon 使用nodeman，将启动项目的node命令替换为nodeman命令即可。 2.路由模块化 为了方便路由进行模块化管理，Express不建议将路由直接挂载到app，推荐将路由抽离为单独模块，步骤如下： 创建路由模块对应的js文件 调用express.Router()函数创建路由对象 向路由对象上挂载具体路由 使用module.exports向外共享路由对象 使用app.use()函数注册路由模块 router.js: const express=require('express') const router=express.Router() router.get('/get',function (req,res) { res.send('这是Get请求') }) router.post('/post',function (req,res) { res.send('这是post请求') }) module.exports=router express.js: const router=require('./router') const express=require('express') const app=express() app.use(router) const server = app.listen(8081, function () { const {address,port} = server.address(); console.log("express server running at https://%s:%s", address, port) }); 3.中间件 当一个请求到达Express的服务器之后，可以连续调用多个中间件，从而对这次请求进行预处理。 Express中间件，本质上就是一个function处理函数，格式如下function(req,res,next)，必须包含next参数，next表示把流转关系转交给下一个中间件或路由。 全局中间件：到达服务器后，都会触发的中间件。 例子：登录拦截器 const express=require('express') const app=express() const bodyParser = require('body-parser'); const cookieParser = require("cookie-parser"); app.use(bodyParser.urlencoded({ extends: false })) app.use(express.static('public')) app.use(cookieParser()) const middleware=function (req,res,next) { let url=req.url const cookie=req.cookies if(url!=='/login'&&cookie.name!=='Aiwin'){ res.location('/') res.setHeader('Location','/') res.send(301) } next() } app.use(middleware) app.get('/',function (req,res) { res.sendFile(__dirname+"index.html") }) app.post('/login',function(req,res){ const username=req.body.username const password=req.body.password if(username==='admin'&&password==='admin'){ res.cookie("name","Aiwin",{maxAge:3600}) res.setHeader('Location','/admin') res.location('/admin') res.send(301) } }) app.get('/admin',function (req,res) { res.sendFile(__dirname+"/public/admin.html") }) const server = app.listen(8082, function () { const {address,port} = server.address(); console.log("express server running at https://%s:%s", address, port) }); admin.html: Express Username: Password: admin.html: Admin 这是登录后的页面 所有的请求都会经过中间件，在没有登录成功，获取cookie的时候，无法访问/admin页面 局部中间件，不使用app.use()定义中间件，使用app.METHOD(PATH，中间件，function)定义局部中间件，只对该路由生效。 const express=require('express') const app=express() const mw=function (req,res,next) { req.a='局部中间件生效' next() } app.get('/',mw,function (req,res) { res.send(req.a) }) app.get('/other',function (req,res) { res.send('局部中间件不生效') }) const server = app.listen(8081, function () { const {address,port} = server.address(); console.log("express server running at https://%s:%s", address, port) }); 中间件注意事项 在路由之前注册中间件(错误级别除外)。 中间件一定要执行next()函数。 连续调用多个中间件，多个中间件共享req，res对象。 中间件分类 应用级别中间件，通过app实例上的中间件。 路由级别中间件，绑定到Router()上的中间件。 错误级别中间件，捕获整个项目发生的异常作物，防止项目崩溃。 const express = require('express') const app = express() // 注意：除了错误级别的中间件，其他的中间件，必须在路由之前进行配置 // 通过 express.json() 这个中间件，解析表单中的 JSON 格式的数据 app.use(express.json()) app.use(express.urlencoded({extended:false})) app.post('/', (req, res) => { // 在服务器，可以使用 req.body 这个属性，来接收客户端发送过来的请求体数据 // 默认情况下，如果不配置解析表单数据中间件，则 req.body 默认等于 undefined console.log(req.body) res.send('OK') }) app.post('/url',function (req,res) { console.log(req.body) res.send('OK') }) app.listen(8081) 错误级别中间件注册到路由之后，res.send()没有执行，因为错误中间件中断掉了后面的执行。 内置中间件 中间件 作用 express.static 托管静态资源中间件 express.json 解析JSON格式请求体数据 express.urlencoded 解析URL-encoded格式请求体数据 const express = require('express') const app = express() // 注意：除了错误级别的中间件，其他的中间件，必须在路由之前进行配置 // 通过 express.json() 这个中间件，解析表单中的 JSON 格式的数据 app.use(express.json()) app.post('/', (req, res) => { // 在服务器，可以使用 req.body 这个属性，来接收客户端发送过来的请求体数据 // 默认情况下，如果不配置解析表单数据中间件，则 req.body 默认等于 undefined console.log(req.body) res.send('OK') }) app.listen(8081) 第三方中间件，非官方内置，由别人开发，可以进行下载使用的，跟第三方模块差不多。 自定义中间件 手动模拟express.urlencoded中间件，解析POST提交到服务器的表单数据。 编写代码 封装为模块 自定义中间件.js const querystring = require("querystring"); const mw=function (req,res,next) { let body='' //监听data事件,把数据切割,避免数据太大,无法接受 req.on('data',(chunk)=>{ body+=chunk }) req.on('end',()=>{ req.body=querystring.parse(body) next() }) } module.exports=mw express.js const express=require('express') const mw=require('./自定义中间件') const app=express() app.use(express.static('public')) app.get('/',(req,res)=>{ console.log(1) res.sendFile(__dirname+"index.html") }) app.post('/login',mw,(req,res)=>{ let response={ "name":req.body.name, "gender":req.body.gender } res.send(response) }) app.listen(3000) 4. 跨域问题 CORS相关响应头： 名称 作用 Access-Control-Allow-Origin 只允许访问该资源的外域URL Access-Control-Allow-Headers 仅支持服务端发送9个请求头，设置此参数额外增加 Access-Control-Allow-Methods 允许客户端请求服务器的方法 CORS跨域请求资源分类： 简单请求，只允许get，post，head，且请求头不超过合规的9个字段，无自定义头部。（一次） 预检请求，请求方式为get，post，head外的请求，请求头包含自定义头部，向服务器发送application/json格式数据。 浏览器会发送OPTION请求进行预检，以获知服务器是否允许该实际请求，服务器响应预检请求后，才会发送真正的请求。（两次） 解决方案：1. CORS中间件，JSONP(有缺陷，只支持POST请求) npm install cors直接使用 router.js: const express=require('express') const router=express.Router() router.get('/get',function (req,res) { const query=req.query res.send({ status:0, message:'GET 请求成功!', data:query }) }) router.post('/post',function (req,res) { const query=req.body res.send({ status:0, message:'POST 请求成功!', data:query }) }) router.delete('/delete',function (req,res) { res.send({ status:0, message:'Delete 请求成功' }) }) module.exports=router express.js: const express=require('express') const app=express() const router=require('./router') const cors=require('cors') app.use(cors()) app.use(express.urlencoded({extended:false})) app.use('/api',router) app.listen(8080) index.html: 跨域问题 GET POST Delete 七、Mysql模块 安装与配置 安装操作Mysql数据库的第三方模块 通过mysql模块连接到Mysql数据库 通过mysql模块执行SQL语句 const mysql=require('mysql') const database=mysql.createPool({ host:'127.0.0.1', //数据库的IP地址 user:'root', //登录用户名 password:'root', //登录密码 database:'node' //要操作的数据库 }) //测试mysql是否连接成功 database.query('SELECT 1',(err,results)=>{ if(err){ return console.log(err.message) } console.log(results) }) 基本使用 mysql模块进行mysql的操作，其实就是通过数据库语句对数据库进行查询，只不过mysql模块负责与数据库交互。 const connect=require('mysql') const database=connect.createPool({ host:'127.0.0.1', //数据库的IP地址 user:'root', //登录用户名 password:'root', //登录密码 database:'node' //要操作的数据库 }) //SELECT查询 database.query('select *from users',(err,results)=>{ if(err){ return console.log(err.message)} console.log(results) }) //INSERT插入数据 const user={username:'Tom',password:'IamTom'} const sqlStr=`INSERT INTO users (username,password) VALUES (?,?)` //const sqlStr=`INSERT INTO users SET?` 插入数据字段对应的时候 //database.query(sqlStr,user,function) database.query(sqlStr,[user.username,user.password],(err,results)=>{ if(err){ return console.log(err.message) } if(results.affectedRows===1){ //results是一个对象,内置了fieldCount、affectedRows、InsertId、serverStatus、 warningCount、 message、 protocol41、changedRows属性 console.log('数据插入成功') } }) //更新数据库对象 const UpdateUser={id:2,username:'Jerry',password:'IamJerry'} const updateStr=`UPDATE users SET username=?,password=? where id=?` // const updateStr=`UPDATE users SET ? where id=?` database.query(updateStr,[UpdateUser.username,UpdateUser.password,UpdateUser.id],(err,results)=>{ if(err){ return console.log(err.message) } if(results.affectedRows===1){ console.log('更新成功') } }) //Delete删除 const DeleteStr=`Delete FROM users where id=?` database.query(DeleteStr,2,(err,results)=>{ if(err){ return console.log(err.message) } if(results.affectedRows===1){ console.log('删除成功') } }) 删除推荐使用标记删除，即使用status等字段标记删除，避免误操作delete直接真删除了 Web开发模式 服务器渲染的Web开发：服务器发送给客户端HTML页面，是在服务器通过字符串拼接，动态生成的，不需要使用ajax额外请求数据 优点： 前端耗时少，服务器负责动态生成HTML内容，浏览器直接渲染页面即可。 利于SEO，服务器响应的是完整的HTML页面内容，爬虫更容易爬取。 缺点： 占用服务器资源，服务器端完成HTML页面拼接，如果请求较多，服务器压力过大。 不利于前后端分离，开发效率低，使用服务器渲染无法完成分工合作，对于前端复杂度高的项目，不利于开发。 前后端分离的Web开发：依赖于Ajax技术的应用，后端只负责提供API接口，前端使用ajax调用接口的技术。 优点： 开发体验好，利于分工，前端负责UI开发，后端专注于API开发，前端选择性更多 用户体验好，实现页面局部刷新。 减轻服务器端渲染压力。 缺点： 不利于SEO，完整的HTML页面需要在客户端动态拼接完成，爬虫无法爬取页面有效信息。 Session认证 通过session-express中间件使用session。 const express=require('express') const app=express() const session=require('express-session') app.use(express.urlencoded({extended:false})) app.use(session({ secret:'Aiwin', resave:false, //是否允许并行发送多个请求 saveUninitialized:true //初始化session时是否保存到存储。默认为true })) app.post('/api/login',(req,res)=>{ if(req.body.username!=='admin'||req.body.password!=='admin'){ return res.send({status:1,message:'登录失败'}) } req.session.user=req.body req.session.islogin=true res.send({status:0,message:'登录成功'}) }) app.get('/api/username',(req,res)=>{ if(!req.session.islogin){ return res.send({status:1,message:'fail'}) } return res.send({status:0,username:req.session.user}) }) app.get('/api/logout',(req,res)=>{ req.session.destroy() res.send({ status:0, message:'退出Session成功' }) }) app.listen(8080) 未登录时： 登录后： JWT Session认证机制需要配合Cookie才能实现，由于Cookie不支持跨域访问，当涉及前端跨域请求后端接口时，需要使用很多额外的配置，才能实现Session认证，此时推荐使用JWT。 工作原理：用户的信息通过Token字符串的形式，保存在客户端浏览器，服务器通过还原token字符串的形式来认证用户身份。 JWT组成部分： JWT=Header(头部).Payload(有效载荷).Signature(签名) 部分 说明 头部Header JWT类型+加密算法，JWT主要加密算法又HS256 RS256 载荷Payload 携带存放的数据和注册声明，包括签发者，接收者，签发时间，过期时间等 签名Signature 使用头部定义的加密算法和密钥生成对数据进行加密生成 使用中间件 jsonwebtoken 生成JWT字符串 express-jwt 用于将JWT字符串解析还原成JSON对象 const express=require('express') const jwt=require('jsonwebtoken') const {expressjwt: expressJWT} = require("express-jwt"); const bodyParser=require('body-parser') const app=express() app.use(bodyParser.urlencoded({ extends: false })) const secretKey='Aiwin' //.unless()指定那些接口不需要访问权限 app.use( expressJWT({ secret: secretKey, algorithms: ["HS256"], }).unless({path:[/^\/api\//]}) ); app.post('/api/login',(req,res)=>{ const user=req.body if(user.username!=='admin'||user.password!=='admin'){ return res.send({status:1,message:'登录失败'}) } const token=jwt.sign({username:user.username},secretKey,{expiresIn:'300s'}) res.send({status:200,message:'登录成功',token:token}) }) app.get('/admin/getinfo',(req,res)=> { res.send({ status:200, message:'获取用户信息成功', data:req.auth }) }) //捕捉解析JWT解析失败 app.use((err,req,res,next)=>{ if(err.name==='UnauthorizedError'){ return res.send({status:401,message:'无效token'}) } res.send({status:500,message:'未知错误'}) }) app.listen(8080) 八、multer模块 用于处理multipart/form-data类型的表单数据,它主要用于上传文件 例子： app.js: const express=require('express') const app=express() app.use('/multer',require("./multer")); app.listen(3000) multer.js: const router = require("express").Router(); const multer = require("multer"); const path=require('path') path.extname('jpg') const fs=require('fs') const upload = multer({ dest:"./upload"//上传文件存放路径 }); const singleMidle = upload.single("singleFile");//一次处理一张 //可同时处理多个上传控件的上传 //实际项目中根据自己的情况，使用以上三种用法之一即可！ router.get("/",(req,res)=>{ res.sendFile(path.join(__dirname,'/public/index.html')) }) router.post("/singup", singleMidle, function (req, res) { let oldname=req.file.path let newname=req.file.path+ path.extname(req.file.originalname) fs.renameSync(oldname,newname) res.send(req.file); }); module.exports = router; index.html: multer的使用方案 九、大事件项目 参考文档：某🐎大事件项目 完成的API接口： 链接：https://pan.baidu.com/s/1CXZvE1uq7AFo5iggCDjKFg?pwd=s483 提取码：s483 十、总结 学的东西都比较基础，上手也是非常快，最后的项目也是相当于把前面复习的都用了一遍，感觉NodeJS开发效率也是很高，有很多第三方中间件能直接引用完成很多东西，不需要再自己写，算是站在别人的肩膀上，自己学的比较粗糙，因为只要求自己能看懂基本的代码即可，真正要学好还得细学。
前言 这学期学习了Linux课程，对Linux的常用命令写个总结。 一、为什么Linux如此流行？ 1.原因 作为目前世界上最受欢迎的系统，近乎世界超级计算机Top10中全是Linux系统的身影。 Linux流行的背后，原因是什么呢?： 1、Linux稳定且有效率。 2、Linux开源且免费，有强大的社区支持。 3、Linux拥有更加安全的用户与文件权限策略。 4、Linux易于维护，拥有超强的定制性 当然，个人认为，Linux流行的关键原因在于开源且免费，强大的社区支持。 2.Linux系统的版本 Linux的发行版本有很多，主要流行的如下： 1.Red Hat Linux 2.CentOs，与RedHat企业版完全兼容 3、Ubuntu、Debian、Kali 4、国内的OpenEuler、OpenAnolis 二、Linux的基础命令 1.目录结构 Linux的目录结构如上，/etc存放着系统配置文件的目录，/home是除root外的其它用户的目录，/dev存放设备文件，dev存放设备文件，boot存启动文件，tmp是临时目录，usr存放库文件，文档，命令，用户数据等，var存放变化的文件，如日志文件等， 目录名 解析 bin 存放普通用户可执行命令 etc 系统的配置文件 root 超级用户目录 dev 系统的一些设备文件，驱动等 boot 系统的启动文件 tmp 临时文件存放目录 usr 存放用户的应用程序和文件 var 存放一些变化的文件，如程序日志等 home 普通用户的目录 run 存放进程产生的临时文件 mnt 临时的挂载文件系统 sbin 存放系统管理员才能执行的命令 2.文件命令 1.ls 列出该目录下的子目录与文件，常用参数： -a 以.开头的隐藏项目也列出 -l 列出目录的详细信息，可直接使用ll 2.pwd 显示当前所处目录 3.cd 切换工作的目录，可使用相对路径与绝对路径，相对路径切换多个目录可以使用../..这种格式 4.touch、mkdir touch是不存在文件则创建文件，存在则修改文件时间。 mkdir是创建目录 5.cat、tail、head、tail、od、tee、more、less cat 直接输出文件的全部内容 head 输出文件开头的内容、-n 指定行数 tail 输出文件末尾的内容，-n输出尾n行 more 按页显示文本文件内容，只允许向前浏览 less 与more类似，允许向前向后浏览 tee 将文件的输出复制到另一个文件 od 将文件内容以指定的禁止显示，-t指定进制 6.rm、cp、mv rm删除某个文件，常用rm -r 用于删除目录 cp复制文件，-r可以复制目录 mv移动文件 7.find、grep、xargs find 搜索文件命令，可通过通配符搜索某个文件 grep，以正则表达式进程全局查找匹配行 命令传递参数的过滤器，可将管道或标准输入数据转换成命令行参数，也可从文件中读取数据 三者合一、可用于搜索文件的内容 8.tar、zip、uzip、gzip 1.tar将文件打包或压缩或解包，压缩文件后缀有gz、bz、xz，常用参数 -c产生.tar打包文件，-x解包、-zjJ分别以gz、bz2、xz打包或解包文件。 2.zip将文件打包，常用-r递归打包目录 3.uzip将文件解包 4.gzip压缩命令，只能压缩文件，不能压缩目录 9.ln 创建软或硬连接，可以理解为相当于创建快捷方式，硬连接的链接文件和被链接文件必须在同一目录且不能建立指向目录的硬连接，链接的源文件消失，链接即失效。常用参数：-s 创建符号链接即软连接，-P，创建直接指向符号链接的硬链接，-f删除已存在的目标文件。 10.lsattr和chattr chattr 与 lsattr 分别用于改变和显示文件属性，与 chmod 命令相比，chmod 只改变文件的读写、执行权限，更底层的属性控制是由 chattr 来改变。 chattr主要常用的参数有：-R 递归改变指定目录下的文件属性，+在原有的参数设定基础上、追加参数，-在原有的参数设定基础上、移除参数。 lsattr常用参数：-R 递归显示目录和文件属性、-a 显示所有隐藏文件属性、 chattr可以改变的文件属性有： 属性 含义 a 只允许向文件追加数据，不允许删除和修改文件内容。如果目录有具有这个属性，系统将只允许在目录下简历和修改文件，不允许删除任何文件。只有root才能设置该属性 c 自动将文件压缩，在读取时自动解压缩 d 在进行文件系统备份时，dump指令忽略此文件 e 在ext文件系统中，表示该文件使用区段(extents)映射磁盘上的块 i 不允许对文件进行任何的修改。对于目录而言，只能修改目录之下的文件，不允许建立和删除文件。只有root能设置此属性 j 设定此参数使得当通过mount参数：data=ordered 或者 data=writeback 挂 载的文件系统，文件在写入时会先被记录(在journal中)。如果filesystem被设定参数为 data=journal，则该参数自动失效 s 系统在删除文件时，使用0填充文件所在的区域 t 文件拥有t属性时，与其它文件合并时末端不会存在局部块碎片 u 与s相反，删除文件时，文件内容其实还存在磁盘中，以便以后能够恢复删除的文件 A 不能修改文件的最后访问时间 D 如果一个目录设置了D属性，任何改变将同步到磁盘；这等价于mount命令中的dirsync选项，同步目录 S Sync，一旦应用程序对文件执行了写操作，则立刻将改动同步到磁盘 T 目录设置T属性，Orlov块分配器将该目录视为目录层次结构的顶部，提示块分配器该目录下的子目录是无关的，将被分散的分配 11.Linux重定向与管道 linux执行命令行即shell命令时会有三种标准文件： 标准输入文件即0，使用<或<< 标准输出文件即代码1，使用>或>> 标准错误输出文件即代码2，使用2>或2>> 注：双符号表示追加 管道： | 将上一次的输出作为下一次的输入 || 当左边命令为假，右边命令才会执行 & 表示任务在后台执行 &&前一条命令执行成功才会执行后门的命令 ;隔开命令，从左到右执行 3.Vim编辑器 一个强大的文本编辑器，常用的操作如下 命令 操作 k/j 向上或下查找 gg 移动到文件的第一行 G 移动到文件的最后一行 /word 搜索某个字符串，n表示到下一个 n 光标向下移动n行 0或$ 移动到当前行首字符或尾字符 dd/ggdG 删除当前行，ndd表示连续删除后n行，ggdG表示删除全部 yy 复制当前行，nyy表示复制下面n行 p/P 将复制内容在下一行粘贴上/在上一行粘贴上 u 还原上一次操作 ctrl+r 重复上一次动作 i 光标前插入文本 x 删除光标字符 R+word 连续替换多个字符 :%s/old/new/g 有$替换整个文档的内容，无%替换该行全部，去掉g表示替换首个 set number 显示行数 vim编辑器异常退出会产生.swp临时文件，需要删除掉 4.关于Linux用户 linux的用户账号的基本信息主要存放在/etc/passwd中，存放方式为 字段1：用户帐号的名称(euler) 字段2：用户密码字串或者密码占位符“x” 字段3：用户帐号的UID号(用户ID，1000) 字段4：所属基本组帐号的GID号(组ID，1000) 字段5：用户全名(euler) 字段6：宿主目录(/home/euler) 字段7：登录Shell信息(/bin/bash) 一般密码文件存放在/etc/shadow中,进行了hash加密 组账号信息一般存放在/etc/group中，存放形式为 字段1：组名。 字段2：组密码：组密码，用x占位。 字段3：组标识符：简称GID，唯一标识一个组。 字段4：组内用户列表：属于这个组的所有用户的列表 其中上两张图的0都表示超级用户或超级用户所在组。 1.添加删除用户/组 useradd：添加用户 usedel：删除用户，常用-r删除用户和主目录 usermod：将用户添加到某个组 groupadd：增加某个组 groupdel：删除某个组 注：以上操作皆要通过root用户完成。 2.关于文件权限（chmod、chown） r 表示读权限，数字4 w 表示写权限，数字2 x 表示执行权限，数字1 图中-rw-r--r--的解析： -表示普通文件 rw-表示文件拥有者权限 r-- 表示文件所属组成员权限 r--表示其它用户的权限 权限赋予命令：chmod 可通过数字直接赋予 chmod 数字 文件名 可通过对用户权限直接赋予 chmod u/g/o/a +/- /r/w/x 文件名 字母 解析 u 所有者 g 组成员 o 其他成员 a 所有人 更改文件和目录所有者和所属组：chown 需要root用户执行，命令格式： chown [-R] [user][:group] 5.进程与系统管理 1.进程管理 一般linux启动一个进程后，会在proc/PID创建一个文件夹，用于进程的管理 常用查看进程的命令，ps aux，a表示显示所有进程，u表示面向用户格式，x表示显示后台进程信息。 字符 解析 USER 表示进程所属用户 PID 进程唯一标识号 %CPU CPU的占用率 %MEM 内存的占用率 VSZ 虚拟内容的占用大小 RSS 实际内存占用大小 TTY 终端类型 Stat 进程状态 START 进程启动时刻 TIME 进程运行时长 top或者htop命令也可用于查看进程的情况，会每隔一段时间进行更新，htop要相对简单一些 终止进程命令：kill PID即可终止进程，-9参数表示强制终止 命令行终止:Ctrl+C与Ctrl+Z，区别在于前者是中断正在执行的命令，后者是将进程挂起，调入后台并停止执行，可通过jobs命令查看后台，或直接fg +序号将后台终止进程恢复 2.定时任务 一般linux的定时任务都会存放在/etc/crontab中，不同的系统会有微笑的不同，日、月、小时任务都分别有不同的文件夹，如cron.daily、cron.hourly等 linux可通过crontab设置定时任务，常用参数-e 设置任务，-l 查看任务计划 设置任务的时间格式为： 分钟 小时 日期 月份 星期 命令 表示周一到周五的早上6点输出wakeup， 代表通配符，表示该范围的任意时间。 3.一些其它命令 free 查看内存使用情况 df 查看磁盘使用情况 du 显示指定目录或文件所占用的磁盘空间 alias 设置别名，支队当前中断生效，要永久使用，需要写入到~/.bashrc中并保存，然后执行 source ~/.bashrc 4.关于环境变量 linux系统与windows系统一样，根据环境变量去寻找命令的执行命令，一直往下找，找不到即报错，找到即执行。 linux的环境变量以:为分割符，可以知道我们命令执行的诸如ls等命令都在/usr/bin目录中，在/usr/bin目录找到原文件进而进行命令行的命令执行。 关于环境变量的设置：利用命令export在原有环境变量上:目录即可，同样要永久生效，需要写入~/.bashrc中 5.Linux防火墙(ufw、iptables) 不同的linux系统一般会使用不同的防火墙工具，如Openeuler/centos会使用firewalld、ubuntu使用ufw等，也可以直接使用iptables进行规则的配置，iptables配置要较为复杂，一般执行有关防火墙的操作都需要sudo权限。 ufw常用命令如下： 命令 解析 ufw status 查看防火墙状态 ufw enable 开启ufw防火墙 ufw disable 关闭ufw防火墙 ufw default allow 默认通过 ufw allow port 允许某个端口 ufw deny port 拒绝某个端口 ufw allow from IP 允许某个IP地址访问本机全部端口 ufw delete allow port 删除一条规则 ufw allow from IP to any port PORT 允许特定的IP/端口组合 关于iptables： iptables是采用数据包过滤机制工作的，所以它会对请求的数据包的包头进行分析，并根据我们预先设定的规则进行匹配来决定是否可以进入主机，一般来说，会一层一层的匹配，若上免的规则明确表示阻止或通过，则不会再匹配下面的规则，配置一般采用先精细后粗犷的规则。 iptables包含了四表五链，分别是： Filter表：过滤数据包 NAT表：用于网络地址转换(IP、端口) Mangle表：修改数据包的服务类型、TTL、并且可以配置路由实现QOS Raw表：决定数据包是否被状态跟踪机制处理 INPUT链 ：处理输入数据包。 OUTPUT链 ：处理输出数据包。 FORWARD链 ：处理转发数据包。 PREROUTING链 ：用于目标地址转换(DNAT)。 POSTOUTING链 ：用于源地址转换(SNAT)。 不同的链路存放着用户设置的规则，这些规则根据功能不同被存放到不同的表中，会按照表的优先级取出对应的规则，进行匹配，表的优先级是 raw->mangle->nat->filter。 各链对应的表如下： 链名 表名 PREROUTING rao、mangle、nat FORWARD mangle、filter INPUT manle、filter OUPUT raw、mangle、nat、filter POSTROUTING mangle、nat 一般数据库进入网卡的处理流程 : 1、首先匹配PREROUTING链，系统根据数据包的目的地址判断接下来的数据过程，可能有三种情况： （1）数据包目的地址是本机，发往INPUT链，匹配INPUT链规则，通过则发给相应本地处理，没有则丢弃。 （2）数据包目的地址不是本机，包被发往FORWARD链，匹配规则，通过则发往相应本地处理，没有则丢弃。 （3）数据包是本地产生的，则发往OUPUT链，匹配OUPUT链的规则，通过则发往相应本地处理，没有则丢弃。 iptables常用命令： iptables -A INPUT -p tcp --dport 80 -j ACCPET 追加规则开放80或某端口 iptables -I INPUT -p icmp -j ACCEPT 根据协议插入一条记录 iptables -D INPUT number 删除INPUT链中第number条记录 iptables --list 查看所有规则 iptables -nL -t 表名 查看某个表名的规则 iptables中包含了可以查看ufw防火墙设置的规则信息。 6.一些基本的系统信息、日志查询 last：查看用户最近登录信息，主要存放在/var/log/wtmp|btmp|utmp中，wtmp存储登录成功信息、btml存储登录失败 lastb，查看显示用户错误的登录列表 lscpu，查看cpu的相关信息，包括型号，内核 uname -a，查看关于操作系统的信息 cat /proc/version，查看操作系统版本 lsmod，查看所有载入系统的模块信息 netstat，查看端口的开放和连接信息 6.磁盘管理 关于磁盘的设备一般存储再/dev/或者/snap/文件夹中。 查看磁盘信息常用命令：lsblk、fdisk sr0是光驱设备即光盘，也成为cdrom，只支持读，不支持写入操作 磁盘分区 磁盘分区可以将硬盘驱动器划分多个逻辑存储单元，单元称为分区。磁盘分区的好处： （1）限制应用或用户可用空间 （2）将操作系统和程序文件与用户文件分隔 （3）创建用于操作系统虚拟内存交换的单独区域 （4）允许同一磁盘不同操作系统启动 关于分区的命名规则： 分区的方案： MPT分区中： SCSI接口硬盘最多有16个分区，主分区最多4个，逻辑分区最多12个。 IDE接口硬盘最多只能有63个分区，主分区最多4个，逻辑分区最多60个 分区最大2T。 GPT分区中： 由LBA规划，每个硬盘前面有34个LBA，后门有33个LBA用于前面的备份。 LBA0用于兼容MBR，不能修改，LBA1称为GPT表头记录，用于记录备份的对应关系和记录表本身的大小，LBA2-LBA33用于记录实际分区信息。 每一个LBA可用记录4个分区表，一共4*32=128个分区表。 每个硬盘最多128个分区(不分类型)，GPT逻辑块地址分配64位，最大支持18EB分区大小。 磁盘分区操作： 首先，增加虚拟磁盘，VMvare操作如下： 命令 解析 parted /磁盘目录 对磁盘进行分区 mklabel+类型 创建新的分区表 mkpart +name+start+end 创建分区 rm+number 删除分区 quit 退出 磁盘挂载 命令 解析 mkfs -t +类型+目录 创建磁盘挂载 mount +目录+挂载点 将磁盘挂载(临时，重启即消失) umount +挂载点 取消磁盘挂载 blkid +磁盘目录 查看磁盘的信息，包括UUID /etc/fstab 永久挂载磁盘，要将挂载目录等写入该文件 非交互式磁盘 简单说，就是交互式需要进入parted的命令行进行操作，非交互式就是不用进去就能操作，加个--script，后面命令跟parted里面的一样即可，如： sudo parted --script 目录 mklabel gpt，创建gpt分区 sudo parted --script 目录 +命令 关于逻辑卷 LVM-逻辑卷管理是Linux环境中对磁盘分区进行管理的一种机制，是建立在硬盘和分区之上、文件系统之下的一个逻辑层，用于提高磁盘分区管理的灵活性，主要有以下概念： 逻辑卷的划分： 常用命令 命令 解释 pvcreate /DEV 创建物理卷 pvs 查看物理卷 pvremove /DEV 删除物理卷 vgcreate VGNAME /DEV 创建卷组 vgextend /DEV 扩展卷组 vgreduce VGNAME /DEV 收缩卷组 vgremove VGNAME 删除卷组 lvcreate -L 10G -n LVNAME VGNAME 创建指定逻辑卷大小和逻辑卷名称的名称 lvs 查看逻辑卷 lvextend -L +10G /DEV 增加10G逻辑卷大小 lvremove /DEV/VGNAME/LVNAME 删除逻辑卷 7.下载软件包命令 linux下载软件包的命令很多，根据Linux系统的不同会使用不同的命令。 OpenEuler：dnf、rpm。 Ubuntu：apt、yum。 Centos：rpm、yum。 Debian：apt。 常用的命令如下： 命令 作用 apt-get update/apt update 更新软件包列表 yum/dnf check-update 更新软件包列表 apt-get/apt upgrade 更新已安装的包 yum/dnf upgrade 更新已安装的包 apt/yum/dnf search 搜索某个包 apt show package 显示包的安装状态 yum info/deplist package 列出包依赖 dnf info package 列出包依赖 dnf repoquery --requires package 列出包依赖 apt/yum/dnf/pkg install package 安装包 apt/yum remove package 删除包 dnf erase package 删除包 rpm -ivh 可视化安装进度安装软件包 rpm -qa 查询所有已安装软件包 rmp -e 卸载指定软件包 三、Linux的Shell编程开发 1.符号变量 符号 介绍 # 注释符 $ 变量符 '' 单引号字符，全部普通字符 "" 双引号字符，可以包括特殊字符如反引号`，变量符$ $? 返回上一个变量返回值 $# 传送给shell程序位置参数的数量 $0 当前执行的进程名 $! 后台运行的最后一个进程号 $$ 当前进程的进程号 $n 第n个参数 $@、 $* 全部位置参数做列表，*做变量 $() 获取命令输出结果 `` 获取命令输出结果 $(()) 算数拓展运算符 -eq、-ne、-lt、le、-gt、-ge 表示相等、不等于、小于、小于或等于、大于、大于或等于 [] 条件表达式 -d 是否是目录，如 if [-f a.sh] -f 是否是普通文件，如if [-d a.txt] -a、-o 逻辑和与逻辑或 2.常用方法 命令 解析 set 查看环境变量 env 查看全局变量 declare 定义变量 unset 删除变量 export 定义环境变量 ~/.bashrc 用户级环境 四、关于git git是分布式的版本控制系统，说白了，就是一种代码管理工具，能够方便程序员在编写代码，未完成时候能够保存但却不影响项目的运行。 git的流程： git的常用命令： 命令 解释 git init 初始化git仓库 git config user.name name 设置目标用户名 git config user.email email 设置目标邮箱 git config --list 查看配置信息 git add 文件名 将文件添加到暂存区 git checkout 暂存区内容恢复到工作区 git commit -m "提交说明" 将文件从暂存区添加到仓库区 git log 查看提交日志 git reset --hard 版本号 将代码恢复到已经提交的某一个版本 git branch 分支名 创建分支 git checkout -b 分支名称 切换分支 git branch -d 分支名称 删除分支 git push URL master 将缓存区内容提交到远程仓库 git pull 将远程代码下载到本地 git remote add 仓库别名 仓库地址 为远程仓库添加别名 git push -u 别名 分支 将缓存区内容提交到仓库 具体操作如提交到Github： 先在github创建仓库，然后在Settings->develop settings->Personal Access token->Generate new token创建一个提交的token 然后就可以在Linux中通过命令行提交。 这里输入的用户名和密码就是刚才生成Token的用户名和token。
十一、综合训练（购物车功能） 实现功能： 手机商品页面展示，加入购物车功能，商品详情页面，清空购物车，删除购物车商品。 数据库准备，准备的数据库有商品数据库，还有存储购物车信息的数据库，使用SqliteOpenHelper进行操作，商品数据库的字段有id，description，price，picPath，购物车字段id，goodsId，count，实现如下： package com.example.shoppingcart.entity; //购物车信息 public class CartInfo { public int id; // 商品编号 public int goodsId; // 商品数量 public int count; public CartInfo(){} public CartInfo(int id, int goodsId, int count) { this.id = id; this.goodsId = goodsId; this.count = count; } } package com.example.shoppingcart.entity; import com.example.shoppingcart.R; import java.util.ArrayList; public class Goodsinfo { public int id; public String name; public String description; public float price; public String picPath; public int pic; private static String[] mNameArray = { "iPhone11", "Mate30", "小米10", "OPPO Reno3", "vivo X30", "荣耀30S" }; // 声明一个手机商品的描述数组 private static String[] mDescArray = { "Apple iPhone11 256GB 绿色 4G全网通手机", "华为 HUAWEI Mate30 8GB+256GB 丹霞橙 5G全网通 全面屏手机", "小米 MI10 8GB+128GB 钛银黑 5G手机 游戏拍照手机", "OPPO Reno3 8GB+128GB 蓝色星夜 双模5G 拍照游戏智能手机", "vivo X30 8GB+128GB 绯云 5G全网通 美颜拍照手机", "荣耀30S 8GB+128GB 蝶羽红 5G芯片 自拍全面屏手机" }; // 声明一个手机商品的价格数组 private static float[] mPriceArray = {6299, 4999, 3999, 2999, 2998, 2399}; // 声明一个手机商品的大图数组 private static int[] mPicArray = { R.drawable.iphone, R.drawable.huawei, R.drawable.xiaomi, R.drawable.oppo, R.drawable.vivo, R.drawable.rongyao }; public static ArrayList<Goodsinfo> getDeafultList(){ ArrayList<Goodsinfo> goodsList=new ArrayList<Goodsinfo>(); for(int i=0;i<mNameArray.length;i++){ Goodsinfo goodsinfo=new Goodsinfo(); goodsinfo.id=i; goodsinfo.name=mNameArray[i]; goodsinfo.description=mDescArray[i]; goodsinfo.price=mPriceArray[i]; goodsinfo.pic=mPicArray[i]; goodsList.add(goodsinfo); } return goodsList; } } package com.example.shoppingcart.database; import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import com.example.shoppingcart.entity.CartInfo; import com.example.shoppingcart.entity.Goodsinfo; import java.util.ArrayList; import java.util.List; public class ShoppingDBHelper extends SQLiteOpenHelper { private static final String DB_NAME="shopping.db"; private static final String TABLE_GOODS_INFO="goods_info"; private static final String TABLE_CART_INFO="cart_info"; private static final int DB_VERSION=2; private static ShoppingDBHelper databaseUserHelper=null; public ShoppingDBHelper(Context context) { super(context,DB_NAME,null,DB_VERSION); } //单例模式获取数据库实例 public static ShoppingDBHelper getInstance(Context context){ if(databaseUserHelper==null){ databaseUserHelper=new ShoppingDBHelper(context); } return databaseUserHelper; } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String sql=""; sql= "CREATE TABLE IF NOT EXISTS "+TABLE_GOODS_INFO+"(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,name VARCHAR NOT NULL,description VARCHAR NOT NULL,price FLOAT NOT NULL,pic_path VARCHAR NOT NULL);"; sqLiteDatabase.execSQL(sql); //购物车信息表 sql= "CREATE TABLE IF NOT EXISTS "+TABLE_CART_INFO+"(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,good_id INTEGER NOT NULL,count INTEGER NOT NULL);"; sqLiteDatabase.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } public void insertGoodsinfos(List<Goodsinfo> list){ SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); try{ database.beginTransaction(); for(Goodsinfo goodsinfo:list){ ContentValues contentValues=new ContentValues(); contentValues.put("name",goodsinfo.name); contentValues.put("description",goodsinfo.description); contentValues.put("price",goodsinfo.price); contentValues.put("pic_path",goodsinfo.picPath); database.insert(TABLE_GOODS_INFO,null,contentValues); } database.setTransactionSuccessful(); } catch (Exception e){ e.printStackTrace(); }finally { database.endTransaction(); } } public List<Goodsinfo> queryAllGoodsInfo(){ String sql="SELECT *FROM "+TABLE_GOODS_INFO; List<Goodsinfo> list=new ArrayList<Goodsinfo>(); SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); Cursor cursor=database.rawQuery(sql,null); while(cursor.moveToNext()) { Goodsinfo goodsinfo = new Goodsinfo(); goodsinfo.id = cursor.getInt(0); goodsinfo.name = cursor.getString(1); goodsinfo.description = cursor.getString(2); goodsinfo.price = cursor.getFloat(3); goodsinfo.picPath = cursor.getString(4); list.add(goodsinfo); } cursor.close(); return list; } public void insertCartInfo(int goodsId) { SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); //购物车不存在该商品,则添加一条信息 CartInfo cartInfo=queryCartInfoByGoodsId(goodsId); ContentValues values=new ContentValues(); values.put("good_id",goodsId); if(cartInfo==null){ values.put("count",1); database.insert(TABLE_CART_INFO,null,values); } //如果购物车有该商品,则更新商品数量 else{ values.put("_id",cartInfo.id); values.put("count",++cartInfo.count); database.update(TABLE_CART_INFO,values,"_id=?",new String[]{String.valueOf(cartInfo.id)}); } } private CartInfo queryCartInfoByGoodsId(int goodsId) { SQLiteDatabase database=databaseUserHelper.getReadableDatabase(); Cursor cursor=database.query(TABLE_CART_INFO,null,"good_id=?",new String[]{String.valueOf(goodsId)},null,null,null,null); CartInfo info=null; if(cursor.moveToNext()){ info=new CartInfo(); info.id=cursor.getInt(0); info.goodsId=cursor.getInt(1); info.count=cursor.getInt(2); } return info; } //统计购物车商品的总数量 public int countCartInfo() { SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); int count=0; String sql="SELECT sum(count) FROM " +TABLE_CART_INFO; @SuppressLint("Recycle") Cursor cursor=database.rawQuery(sql,null); if(cursor.moveToNext()){ cursor.getInt(0); } return count; } public List<CartInfo> queryAllCartInfo() { SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); List<CartInfo> list=new ArrayList<CartInfo>(); @SuppressLint("Recycle") Cursor cursor=database.query(TABLE_CART_INFO,null,null,null,null,null,null); while(cursor.moveToNext()){ CartInfo info=new CartInfo(); info.id=cursor.getInt(0); info.goodsId=cursor.getInt(1); info.count=cursor.getInt(2); list.add(info); } return list; } public Goodsinfo queryAllGoodsInfoById(int goodsId) { Goodsinfo info=null; SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); @SuppressLint("Recycle") Cursor cursor=database.query(TABLE_GOODS_INFO,null,"_id=?",new String[]{String.valueOf(goodsId)},null,null,null); if(cursor.moveToNext()){ info = new Goodsinfo(); info.id = cursor.getInt(0); info.name = cursor.getString(1); info.description = cursor.getString(2); info.price = cursor.getFloat(3); info.picPath = cursor.getString(4); } return info; } //根据商品ID删除购物车信息 public void deleteCartInfoByGoodsId(int goodsId) { SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); database.delete(TABLE_CART_INFO,"good_id=?",new String[]{String.valueOf(goodsId)}); } //删除所有购物车信息 public void deleteAllCartInfo(){ SQLiteDatabase database=databaseUserHelper.getWritableDatabase(); database.delete(TABLE_CART_INFO,"1=1",null); } } Activity类主要有4个，分别为MyApplication，购物车类，商品市场类，商品详情类，MyApplication启动时自动将商品数据插入到数据库，并设置购物车全局变量为0. 商品类的展示再通过从数据库中获取存储的商品信息，通过获取商品的子视图，动态的嵌入到页面中，当点击加入购物车时，将点击的商品加入到购物车数据库，随后购物车全局变量+1，显示出来。 购物车功能实现思路：从购物车数据库中取出商品的good_id，从商品数据库中查询出商品的全部信息，然后动态嵌入子视图进行展示。清空功能则直接清空购物车数据库，并将购物车全局变量归0，长按点击删除实现思路则是对嵌入的view进行长监听，长按点击则弹窗，如果删除则从购物车数据库中删除，并将view子视图移除，Myapplication全局变量-1。 商品详情页实现思路：利用Intent传一个商品的good_id进入到商品详情页，通过good_id查询商品的信息，并赋到控件中展示。 MyApplication类： package com.example.shoppingcart; import android.app.Application; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Environment; import android.util.Log; import com.example.shoppingcart.database.ShoppingDBHelper; import com.example.shoppingcart.entity.Goodsinfo; import com.example.shoppingcart.util.FileUtil; import com.example.shoppingcart.util.SharedUtil; import java.io.File; import java.util.List; public class MyApplication extends Application { private static MyApplication mApp; public int goodsCount; public static MyApplication getInstance(){ return mApp; } public void onCreate() { super.onCreate(); mApp=this; Log.d("App状态:","App启动"); InitGoodsInfo(); } private void InitGoodsInfo() { boolean isFirst= SharedUtil.getInstance(this).readBoolean("first",true); String directory=getExternalFilesDir((Environment.DIRECTORY_DOWNLOADS)).toString()+ File.separatorChar; if(isFirst){ List<Goodsinfo> list=Goodsinfo.getDeafultList(); for(Goodsinfo goodsinfo:list){ Bitmap bitmap= BitmapFactory.decodeResource(getResources(),goodsinfo.pic); String path=directory+goodsinfo.id+".jpg"; FileUtil.saveImage(path,bitmap); bitmap.recycle(); goodsinfo.picPath=path; } //打开数据库将商品信息插入表中 ShoppingDBHelper shoppingDBHelper=ShoppingDBHelper.getInstance(this); shoppingDBHelper.getWritableDatabase(); shoppingDBHelper.insertGoodsinfos(list); shoppingDBHelper.close(); SharedUtil.getInstance(this).writeBoolean("first",false); } } } 商品市场类： package com.example.shoppingcart; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.GridLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.example.shoppingcart.database.ShoppingDBHelper; import com.example.shoppingcart.entity.Goodsinfo; import com.example.shoppingcart.util.ToastUtil; import java.util.List; public class ShoppingChannelActivity extends AppCompatActivity implements View.OnClickListener{ private ShoppingDBHelper shoppingDBHelper; private TextView tv_count; private GridLayout gl_Channel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shopping_channel); shoppingDBHelper=ShoppingDBHelper.getInstance(this); shoppingDBHelper.getWritableDatabase(); shoppingDBHelper.getReadableDatabase(); TextView tv_title=findViewById(R.id.tv_title); tv_title.setText("手机商场"); tv_count=findViewById(R.id.tv_count); gl_Channel=findViewById(R.id.gl_channel); findViewById(R.id.iv_back).setOnClickListener(this); findViewById(R.id.iv_cart).setOnClickListener(this); showGoods(); } //通过手机视图根文件的形式,动态查询数据库,并嵌入到页面中 private void showGoods() { //获取屏幕宽度 int screenWidth=getResources().getDisplayMetrics().widthPixels; LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(screenWidth/2,LinearLayout.LayoutParams.WRAP_CONTENT); //查询数据库所有商品记录 List<Goodsinfo> list=shoppingDBHelper.queryAllGoodsInfo(); gl_Channel.removeAllViews(); for(Goodsinfo goodsinfo:list){ //获取布局文件item_goods.xml的根视图 @SuppressLint("InflateParams") View view= LayoutInflater.from(this).inflate(R.layout.item_goods,null); ImageView iv_thumb=view.findViewById(R.id.iv_thumb); TextView tv_name=view.findViewById(R.id.tv_name); TextView tv_price=view.findViewById(R.id.tv_price); Button btn_add=view.findViewById(R.id.btn_add); iv_thumb.setImageURI(Uri.parse(goodsinfo.picPath)); tv_name.setText(goodsinfo.name); tv_price.setText(String.valueOf((int) goodsinfo.price)); btn_add.setOnClickListener(v->{ addToCart(goodsinfo.id,goodsinfo.name); }); iv_thumb.setOnClickListener(v->{ Intent intent=new Intent(ShoppingChannelActivity.this,ShoppingDetailActivity.class); intent.putExtra("good_id",goodsinfo.id); startActivity(intent); }); //把商品视图加到网格布局,设置屏幕的宽高 gl_Channel.addView(view,params); } } private void addToCart(int goodsId,String name){ shoppingDBHelper.insertCartInfo(goodsId); int count=++MyApplication.getInstance().goodsCount; tv_count.setText(String.valueOf(count)); ToastUtil.show(this,"已添加一部"+name+"到购物车"); } @Override protected void onResume() { super.onResume(); Log.d("返回了","返回了页面"); //查询商品总数并展示 showCartInfoTotal(); } private void showCartInfoTotal() { int count=shoppingDBHelper.countCartInfo(); MyApplication.getInstance().goodsCount=count; Log.d("count:", String.valueOf(count)); tv_count.setText(String.valueOf(count)); } @Override protected void onDestroy() { super.onDestroy(); shoppingDBHelper.close(); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.iv_back: finish(); break; case R.id.iv_cart: Intent intent=new Intent(this,ShoppingcartActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; } } } 购物车类： package com.example.shoppingcart; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.example.shoppingcart.database.ShoppingDBHelper; import com.example.shoppingcart.entity.CartInfo; import com.example.shoppingcart.entity.Goodsinfo; import com.example.shoppingcart.util.ToastUtil; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; public class ShoppingcartActivity extends AppCompatActivity implements View.OnClickListener { private TextView tv_count; private LinearLayout ll_cart; private ShoppingDBHelper shoppingDBHelper; private List<CartInfo> mCartList; private TextView tv_total_price; private LinearLayout ll_empty; private LinearLayout ll_content; //声明一个根据商品编号查找商品信息的映射,把商品信息缓存起来 private final Map<Integer,Goodsinfo> goodsinfoMap=new HashMap<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shoppingcart); tv_count=findViewById(R.id.tv_count); TextView tv_title=findViewById(R.id.tv_title); tv_title.setText("购物车"); ll_cart=findViewById(R.id.ll_cart); tv_total_price=findViewById(R.id.tv_total_price); findViewById(R.id.iv_back).setOnClickListener(this); findViewById(R.id.btn_shopping_channel).setOnClickListener(this); findViewById(R.id.btn_clear).setOnClickListener(this); findViewById(R.id.btn_settle).setOnClickListener(this); tv_count.setText(String.valueOf(MyApplication.getInstance().goodsCount)); shoppingDBHelper=ShoppingDBHelper.getInstance(this); ll_empty=findViewById(R.id.ll_empty); ll_content=findViewById(R.id.ll_content); } @Override protected void onResume() { super.onResume(); showCart(); } //展示购物车中的商品列表 private void showCart() { //移除下面所有子视图 ll_cart.removeAllViews(); mCartList=shoppingDBHelper.queryAllCartInfo(); if(mCartList.size()==0){ return ; } for(CartInfo info:mCartList ){ //根据商品编号查询商品数据库中的记录 Goodsinfo goods=shoppingDBHelper.queryAllGoodsInfoById(info.goodsId); goodsinfoMap.put(info.goodsId,goods); View view=LayoutInflater.from(this).inflate(R.layout.item_cart,null); ImageView iv_thumb=view.findViewById(R.id.iv_thumb); TextView tv_name=view.findViewById(R.id.tv_name); TextView tv_desc=view.findViewById(R.id.tv_desc); TextView tv_count=view.findViewById(R.id.tv_price); @SuppressLint("CutPasteId") TextView tv_price=view.findViewById(R.id.tv_price); TextView tv_sum=view.findViewById(R.id.tv_sum); iv_thumb.setImageURI(Uri.parse(goods.picPath)); tv_name.setText(goods.name); tv_desc.setText(goods.description); tv_count.setText(String.valueOf(info.count)); tv_price.setText(String.valueOf((int) goods.price)); tv_sum.setText(String.valueOf((int) (info.count*goods.price))); //给商品行添加长按事件,长按商品就删除该商品 view.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(final View v) { AlertDialog.Builder builder = new AlertDialog.Builder(ShoppingcartActivity.this); builder.setMessage("是否从购物车删除"+goods.name+"？"); builder.setPositiveButton("是", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ll_cart.removeView(v); // 移除当前视图 deleteGoods(info); // 删除该商品 } }); builder.setNegativeButton("否", null); builder.create().show(); // 显示提醒对话框 return true; } }); view.setOnClickListener(v->{ Intent intent = new Intent(ShoppingcartActivity.this, ShoppingDetailActivity.class); intent.putExtra("good_id", goods.id); startActivity(intent); }); ll_cart.addView(view); } //重新计算购物车商品总金额 refreshTotalPrice(); } private void deleteGoods(CartInfo info) { MyApplication.getInstance().goodsCount-=info.count; //从购物车数据库中删除商品 shoppingDBHelper.deleteCartInfoByGoodsId(info.goodsId); //从购物车列表中删除商品 CartInfo removed=null; for(CartInfo cartInfo:mCartList){ if(cartInfo.goodsId==info.goodsId){ removed=cartInfo; break; } } Log.d("delete执行:","执行"); mCartList.remove(removed); //显示最新商品数量 showCount(); ToastUtil.show(this,"已从购物车中删除"+ Objects.requireNonNull(goodsinfoMap.get(info.goodsId)).name); goodsinfoMap.remove(info.goodsId); refreshTotalPrice(); } private void showCount() { tv_count.setText(String.valueOf(MyApplication.getInstance().goodsCount)); //购物车中没有商品,显示空空如也 if(MyApplication.getInstance().goodsCount==0){ ll_empty.setVisibility(View.VISIBLE); ll_content.setVisibility(View.GONE); ll_cart.removeAllViews(); }else{ ll_content.setVisibility(View.VISIBLE); ll_empty.setVisibility(View.GONE); } } private void refreshTotalPrice() { int totalPrice=0; for(CartInfo info:mCartList){ Goodsinfo goods=goodsinfoMap.get(info.goodsId); totalPrice+=goods.price*info.count; } tv_total_price.setText(String.valueOf(totalPrice)); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.iv_back: finish(); break; case R.id.btn_shopping_channel: Intent intent=new Intent(this,ShoppingChannelActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; case R.id.btn_clear: shoppingDBHelper.deleteAllCartInfo(); MyApplication.getInstance().goodsCount=0; showCount(); break; case R.id.btn_settle: AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setTitle("结算商品"); builder.setMessage("支付功能尚未开通"); builder.setPositiveButton("我知道了",null); builder.create().show(); break; } } } 商品详情页类： package com.example.shoppingcart; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.example.shoppingcart.database.ShoppingDBHelper; import com.example.shoppingcart.entity.Goodsinfo; import com.example.shoppingcart.util.ToastUtil; import org.w3c.dom.Text; public class ShoppingDetailActivity extends AppCompatActivity implements View.OnClickListener { private TextView tv_count; private TextView tv_goods_price; private TextView tv_goods_desc; private ImageView tv_goods_pic; private ShoppingDBHelper shoppingDBHelper; private int mGoodsId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_shopping_detail); tv_count=findViewById(R.id.tv_count); tv_goods_desc=findViewById(R.id.tv_goods_desc); tv_goods_price=findViewById(R.id.tv_goods_price); tv_goods_pic=findViewById(R.id.iv_goods_pic); TextView tv_title=findViewById(R.id.tv_title); tv_title.setText("商品详情"); findViewById(R.id.iv_back).setOnClickListener(this); findViewById(R.id.iv_cart).setOnClickListener(this); findViewById(R.id.btn_add_cart).setOnClickListener(this); shoppingDBHelper=ShoppingDBHelper.getInstance(this); } @Override protected void onResume() { super.onResume(); showDetail(); } private void showDetail() { //获取传来的商品编号,通过商品编号来展示页面 mGoodsId=getIntent().getIntExtra("good_id",0); if(mGoodsId>0){ Goodsinfo goodsinfo=shoppingDBHelper.queryAllGoodsInfoById(mGoodsId); tv_goods_desc.setText(goodsinfo.description); tv_goods_price.setText(String.valueOf(goodsinfo.price)); tv_goods_pic.setImageURI(Uri.parse(goodsinfo.picPath)); } } @Override public void onClick(View view) { switch (view.getId()){ case R.id.iv_back: finish(); break; case R.id.iv_cart: Intent intent = new Intent(this, ShoppingcartActivity.class); startActivity(intent); break; case R.id.btn_add_cart: addToCart(mGoodsId); break; } } private void addToCart(int mGoodsId) { int count = ++MyApplication.getInstance().goodsCount; tv_count.setText(String.valueOf(count)); shoppingDBHelper.insertCartInfo(mGoodsId); ToastUtil.show(this, "成功添加至购物车"); } } 商品展示视图： <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/orange" android:orientation="vertical" > <include layout="@layout/title_shopping"/> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <GridLayout android:id="@+id/gl_channel" android:layout_width="match_parent" android:layout_height="wrap_content" android:columnCount="2"/> </ScrollView> </LinearLayout> 购物车展示视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/orange" android:orientation="vertical"> <include layout="@layout/title_shopping" /> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:id="@+id/ll_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="visible"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="85dp" android:layout_height="wrap_content" android:gravity="center" android:text="图片" android:textColor="@color/black" android:textSize="15sp" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="3" android:gravity="center" android:text="名称" android:textColor="@color/black" android:textSize="15sp" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="数量" android:textColor="@color/black" android:textSize="15sp" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="单价" android:textColor="@color/black" android:textSize="15sp" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="总价" android:textColor="@color/black" android:textSize="15sp" /> </LinearLayout> <LinearLayout android:id="@+id/ll_cart" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="0dp"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center|right" android:text="总金额：" android:textColor="@color/black" android:textSize="17sp" /> <TextView android:id="@+id/tv_total_price" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:gravity="center|left" android:textColor="@color/red" android:textSize="25sp" /> </LinearLayout> </LinearLayout> <LinearLayout android:id="@+id/ll_empty" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone" tools:visibility="visible"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="100dp" android:layout_marginBottom="100dp" android:gravity="center" android:text="哎呀，购物车空空如也，快去选购商品吧" android:textColor="@color/black" android:textSize="17sp" /> </LinearLayout> </RelativeLayout> </ScrollView> </LinearLayout> 商品详情视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/orange" android:orientation="vertical"> <include layout="@layout/title_shopping" /> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <ImageView android:id="@+id/iv_goods_pic" android:layout_width="match_parent" android:layout_height="350dp" android:scaleType="fitCenter" tools:src="@drawable/xiaomi" /> <TextView android:id="@+id/tv_goods_price" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="5dp" android:textColor="@color/red" android:textSize="22sp" tools:text="1990" /> <TextView android:id="@+id/tv_goods_desc" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="5dp" android:textColor="@color/black" android:textSize="15sp" tools:text="小米 MI10 8GB+128GB 钛银黑 5G手机 游戏拍照手机" /> </LinearLayout> </ScrollView> </LinearLayout> 单一商品视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/ll_item" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:background="@color/white" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="小米手机" /> <ImageView android:id="@+id/iv_thumb" android:layout_width="180dp" android:layout_height="150dp" android:scaleType="fitCenter" tools:src="@drawable/xiaomi" /> <LinearLayout android:layout_width="match_parent" android:layout_height="45dp" android:orientation="horizontal"> <TextView android:id="@+id/tv_price" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:gravity="center" android:textColor="@color/red" android:textSize="15sp" tools:text="20" /> </LinearLayout> </LinearLayout> 购物车标志视图： <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="50dp" android:background="#aaaaff"> <ImageView android:id="@+id/iv_back" android:layout_width="50dp" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:padding="10dp" android:scaleType="fitCenter" android:src="@drawable/ic_back"/> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_centerInParent="true" android:gravity="center" android:textColor="@color/black" android:textSize="20sp" /> <ImageView android:id="@+id/iv_cart" android:layout_width="50dp" android:layout_height="match_parent" android:layout_alignParentRight="true" android:scaleType="fitCenter" android:src="@drawable/cart" /> <TextView android:id="@+id/tv_count" android:layout_width="20dp" android:layout_height="20dp" android:layout_alignParentTop="true" android:layout_toRightOf="@+id/iv_cart" android:layout_marginLeft="-20dp" android:gravity="center" android:background="@drawable/shape_oval_red" android:text="0" android:textColor="@color/white" android:textSize="15sp" /> </RelativeLayout> 单一商品子视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/ll_item" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:background="@color/white" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="小米手机" /> <ImageView android:id="@+id/iv_thumb" android:layout_width="180dp" android:layout_height="150dp" android:scaleType="fitCenter" tools:src="@drawable/xiaomi" /> <LinearLayout android:layout_width="match_parent" android:layout_height="45dp" android:orientation="horizontal"> <TextView android:id="@+id/tv_price" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:gravity="center" android:textColor="@color/red" android:textSize="15sp" tools:text="20" /> </LinearLayout> </LinearLayout> 购物车展示单一商品子视图： <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" android:orientation="horizontal"> <ImageView android:id="@+id/iv_thumb" android:layout_width="85dp" android:layout_height="85dp" android:scaleType="fitCenter" tools:src="@drawable/xiaomi"/> <LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="3" android:orientation="vertical"> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2" android:gravity="left|center" android:textColor="@color/black" android:textSize="17sp" tools:text="小米手机"/> <TextView android:id="@+id/tv_desc" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="3" android:gravity="left|center" android:textColor="@color/black" android:textSize="12sp" tools:text="小米 MI10 8GB+128GB 钛银黑 5G手机 游戏拍照手机"/> </LinearLayout> <TextView android:id="@+id/tv_count" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="2"/> <TextView android:id="@+id/tv_price" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="right|center" android:textColor="@color/black" android:textSize="15sp" tools:text="1000"/> <TextView android:id="@+id/tv_sum" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1.2" android:gravity="right|center" android:textColor="@color/red" android:textSize="17sp" tools:text="2000"/> </LinearLayout> 十二、内容提供者Provider 为App存取内部数据提供统一的外部接口，让不同应用之间得以共享数据，将用户的输入内容，通过ContentProvider跨进程通信传递到Server App，主要通过Uri作为地址传输，客户端通过ProviderResolver类进行操作，Uri格式如下： content://authority/data_path/id content:// 通用前缀，标识Uri用于ContentProvider定位资源 authority 授权者名称，用于确定具体由哪一个ContentProvider提供资源 data_path 数据路径 id 数据编号，用来请求单条数据 例子：创建server和client端，从client端操作server端的数据库，达到添加，查询，删除用户功能 Server: DatabaseUserHelper.java package com.example.server_provider.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import java.util.ArrayList; import java.util.List; public class DatabaseUserHelper extends SQLiteOpenHelper { private static final String DB_NAME="user.db"; public static final String TABLE_NAME="user_info"; private static final int DB_VERSION=2; private static DatabaseUserHelper databaseUserHelper=null; public DatabaseUserHelper(Context context) { super(context,DB_NAME,null,DB_VERSION); } //单例模式获取数据库实例 public static DatabaseUserHelper getInstance(Context context){ if(databaseUserHelper==null){ databaseUserHelper=new DatabaseUserHelper(context); } return databaseUserHelper; } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String sql= "CREATE TABLE IF NOT EXISTS "+TABLE_NAME+"(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,name VARCHAR NOT NULL,age INTEGER NOT NULL,height LONG NOT NULL,weight float NOT NULL,married INTEGER NOT NULL);"; sqLiteDatabase.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } } provider类: package com.example.server_provider.provider; import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.util.Log; import com.example.server_provider.database.DatabaseUserHelper; public class UserInfoProvider extends ContentProvider { private DatabaseUserHelper databaseUserHelper; public static final String AUTHORITIES = "com.example.server_provider.provider.UserInfoProvider"; private static final UriMatcher URI_MATCHER=new UriMatcher(UriMatcher.NO_MATCH); private static final int USERS=1; private static final int USER=2; static { //Uri匹配器 URI_MATCHER.addURI(AUTHORITIES,"/user",USERS); URI_MATCHER.addURI(AUTHORITIES,"user/#",USER); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int count=0; switch (URI_MATCHER.match(uri)){ case USERS: SQLiteDatabase db1=databaseUserHelper.getWritableDatabase(); count=db1.delete(DatabaseUserHelper.TABLE_NAME,selection,selectionArgs); db1.close(); break; case USER: String id=uri.getLastPathSegment(); SQLiteDatabase db2=databaseUserHelper.getWritableDatabase(); count=db2.delete(DatabaseUserHelper.TABLE_NAME,"_id=?",new String[]{id}); db2.close(); break; } return count; } @Override public String getType(Uri uri) { // TODO: Implement this to handle requests for the MIME type of the data // at the given URI. throw new UnsupportedOperationException("Not yet implemented"); } @Override public Uri insert(Uri uri, ContentValues values) { if(URI_MATCHER.match(uri)==USERS) { SQLiteDatabase db = databaseUserHelper.getWritableDatabase(); db.insert(DatabaseUserHelper.TABLE_NAME, null, values); } return uri; } @Override public boolean onCreate() { Log.d("Provider","Provider启动"); databaseUserHelper=DatabaseUserHelper.getInstance(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if(URI_MATCHER.match(uri)==USERS) { SQLiteDatabase db = databaseUserHelper.getWritableDatabase(); return db.query(DatabaseUserHelper.TABLE_NAME, projection, selection, selectionArgs, null, null, null); } return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO: Implement this to handle requests to update one or more rows. throw new UnsupportedOperationException("Not yet implemented"); } } client端操作类： package com.example.client_provider; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.example.client_provider.entity.User; import com.example.client_provider.util.ToastUtil; public class ContentWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private Button save; private Button delete; private Button query; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_content_write); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); save=findViewById(R.id.save); delete=findViewById(R.id.delete); query=findViewById(R.id.query); save.setOnClickListener(this); delete.setOnClickListener(this); query.setOnClickListener(this); } @SuppressLint({"NonConstantResourceId", "Range"}) @Override public void onClick(View view) { switch (view.getId()){ case R.id.save: ContentValues values=new ContentValues(); values.put(UserInfoContent.NAME,name.getText().toString()); values.put(UserInfoContent.AGE,Integer.parseInt(age.getText().toString())); values.put(UserInfoContent.HEIGHT,height.getText().toString()); values.put(UserInfoContent.WEIGHT,weight.getText().toString()); values.put(UserInfoContent.MARRIED,married.isChecked()); getContentResolver().insert(UserInfoContent.CONTENT_Uri,values); ToastUtil.show(this,"保存成功"); break; case R.id.query: Cursor cursor=getContentResolver().query(UserInfoContent.CONTENT_Uri,null,null,null,null); if(cursor!=null){ while (cursor.moveToNext()){ User info=new User(); info.id=cursor.getInt(cursor.getColumnIndex(UserInfoContent._ID)); info.name=cursor.getString(cursor.getColumnIndex(UserInfoContent.NAME)); info.age=cursor.getInt(cursor.getColumnIndex(UserInfoContent.AGE)); info.height=cursor.getInt(cursor.getColumnIndex(UserInfoContent.HEIGHT)); info.weight=cursor.getFloat(cursor.getColumnIndex(UserInfoContent.WEIGHT)); info.married= cursor.getInt(cursor.getColumnIndex(UserInfoContent.MARRIED)) == 1; Log.d("查询结果:",info.toString()); } cursor.close(); } break; case R.id.delete: //删除id为2的 Uri uri= Uri.parse("content://com.example.server_provider.provider.UserInfoProvider/user/2"); //删除全 int count = getContentResolver().delete(UserInfoContent.CONTENT_Uri, "name=?", new String[]{"Aiwin"}); if(count>0){ ToastUtil.show(this,"删除成功"); } break; } } } 运行时动态申请权限 Android系统未了防止某些App滥用权限，从6.0开始引入运行时权限管理机制，允许App在允许过程中拥有某项权限，一旦缺少，可自动弹出小窗口提示开启权限， 也可在应用启动时候即申请全部权限，一般流程如下： 检查App是否开启指定权限，调用ContextCompat的checkSelfPermission方法 请求系统弹窗，便于用户选择，调用ActivityCompat的requestPermissions方法，命令系统弹出申请窗口 重写获得页面的权限请求回调方法onRequestPermissionsResult，在该方法内部处理用户权限选择结果。 例子：申请短信读写、联系人读写权限 package com.example.client_provider; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.provider.Settings; import android.util.Log; import android.view.View; import com.example.client_provider.util.PermissionUtil; import com.example.client_provider.util.ToastUtil; public class PermissionHungryActivity extends AppCompatActivity implements View.OnClickListener{ private static final int RequestAll=0; private static final String[] PERMISSION_ALL=new String[]{ Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_SMS, Manifest.permission.SEND_SMS }; private static final String[] PERMISSION_CONTACT=new String[]{ Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS }; private static final int RequestCodeConTact=1; private static final String[] PERMISSION_SMS=new String[]{ Manifest.permission.READ_SMS, Manifest.permission.SEND_SMS }; private static final int RequestCodeSMS=2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_permission_lazy); findViewById(R.id.btn_contact).setOnClickListener(this); findViewById(R.id.btn_sms).setOnClickListener(this); PermissionUtil.CheckPermissions(this,PERMISSION_ALL,RequestAll); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.btn_contact: PermissionUtil.CheckPermissions(this,PERMISSION_CONTACT,RequestCodeConTact); break; case R.id.btn_sms: PermissionUtil.CheckPermissions(this,PERMISSION_SMS,RequestCodeSMS); break; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode){ case RequestAll: if(PermissionUtil.checkGrant(grantResults)){ Log.d("权限情况","所有权限获取成功"); } else{ for(int i=0;i<permissions.length;i++){ if(grantResults[i]!= PackageManager.PERMISSION_GRANTED){ switch (permissions[i]){ case Manifest.permission.READ_CONTACTS: case Manifest.permission.WRITE_CONTACTS: ToastUtil.show(this,"通讯录读写获取不成功"); JumpToSettings(); return ; case Manifest.permission.READ_SMS: case Manifest.permission.SEND_SMS: ToastUtil.show(this,"短信读写获取不成功"); JumpToSettings(); return ; } } } } case RequestCodeConTact: if(PermissionUtil.checkGrant(grantResults)) { Log.d("权限情况:","通讯录权限请求成功"); } else{ ToastUtil.show(this,"通讯录获取失败"); JumpToSettings(); } break; case RequestCodeSMS: if(PermissionUtil.checkGrant(grantResults)) { Log.d("权限情况:","短信权限请求成功"); } else{ ToastUtil.show(this,"短信获取失败"); JumpToSettings(); } break; } } public void JumpToSettings(){ Intent intent=new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.fromParts("package", getPackageName(), null)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } } package com.example.client_provider.util; import android.app.Activity; import android.content.pm.PackageManager; import android.os.Build; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; public class PermissionUtil { public static boolean CheckPermissions(Activity activity, String[] permissions, int RequestCode){ if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){ int check= PackageManager.PERMISSION_GRANTED; for(String permission:permissions ){ check=ContextCompat.checkSelfPermission(activity,permission); if(check!=PackageManager.PERMISSION_GRANTED){ break; } } //未开启权限则自动弹窗 if(check!=PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(activity,permissions,RequestCode); return false; } } return true; } public static boolean checkGrant(int[] grantResults) { if(grantResults!=null){ for(int grantResult :grantResults){ if(grantResult!=PackageManager.PERMISSION_GRANTED){ return false; } } return true; } return false; } } 添加查询联系人 利用ContentResolver读写联系人，联系人分为两张表： raw_contacts表：记录联系人的_id号，状态等信息 data表：记录用户通讯录的所有数据，根据mimetype_id表示不同的数据类型，raw_contact_id与raw_contacts表中的_id对应，根据这种外键联系， 一个联系人可添加多个邮箱，多个电话号码。 例子：添加和查询联系人，前提：权限已开启 package com.example.client_provider; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.OperationApplicationException; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.util.Log; import android.view.View; import android.widget.EditText; import android.provider.ContactsContract; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.CommonDataKinds; import com.example.client_provider.entity.Contact; import java.util.ArrayList; public class ContactAddActivity extends AppCompatActivity implements View.OnClickListener { private EditText et_contact_name; private EditText et_contact_phone; private EditText et_contact_email; @SuppressLint({"WrongViewCast", "MissingInflatedId"}) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_contact_add); et_contact_name=findViewById(R.id.tv_name); et_contact_phone=findViewById(R.id.phone); et_contact_email=findViewById(R.id.email); findViewById(R.id.add).setOnClickListener(this); findViewById(R.id.query).setOnClickListener(this); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.add: Contact contact=new Contact(); contact.name=et_contact_name.getText().toString(); contact.phone=et_contact_phone.getText().toString(); contact.email=et_contact_email.getText().toString(); //使用ContentSolver多次写入 //addContacts(getContentResolver(),contact); //批量处理联系人,好处是，要么全部成功，要么全部失败，保证了事务的一致性 addFullContacts(getContentResolver(), contact); break; case R.id.query: readPhoneContact(getContentResolver()); break; } } private void readPhoneContact(ContentResolver resolver) { Cursor cursor = resolver.query(ContactsContract.RawContacts.CONTENT_URI, new String[]{ContactsContract.RawContacts._ID}, null, null, null, null); while (cursor.moveToNext()) { int rawContactId = cursor.getInt(0); Uri uri = Uri.parse("content://com.android.contacts/contacts/" + rawContactId + "/data"); Cursor dataCursor = resolver.query(uri, new String[]{Contacts.Data.MIMETYPE, Contacts.Data.DATA1, Contacts.Data.DATA2}, null, null, null); Contact contact = new Contact(); while (dataCursor.moveToNext()) { @SuppressLint("Range") String data1 = dataCursor.getString(dataCursor.getColumnIndex(Contacts.Data.DATA1)); @SuppressLint("Range") String mimeType = dataCursor.getString(dataCursor.getColumnIndex(Contacts.Data.MIMETYPE)); switch (mimeType) { case CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE: contact.name = data1; break; case CommonDataKinds.Email.CONTENT_ITEM_TYPE: contact.email = data1; break; case CommonDataKinds.Phone.CONTENT_ITEM_TYPE: contact.phone = data1; break; } } dataCursor.close(); if (contact.name != null) { Log.d("联系人:", contact.toString()); } } cursor.close(); } private void addFullContacts(ContentResolver contentResolver, Contact contact) { ContentProviderOperation op_main=ContentProviderOperation .newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME,null) .build(); //联系人姓名记录操作器 ContentProviderOperation op_name = ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) // 将第0个操作的id，即 raw_contacts 的 id 作为 data 表中的 raw_contact_id .withValueBackReference(Contacts.Data.RAW_CONTACT_ID, 0) .withValue(Contacts.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(Contacts.Data.DATA2, contact.name) .build(); ContentProviderOperation op_phone=ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(Contacts.Data.RAW_CONTACT_ID,0) .withValue(Contacts.Data.MIMETYPE,CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(Contacts.Data.DATA1, contact.email) .withValue(Contacts.Data.DATA2, CommonDataKinds.Email.TYPE_WORK) .build(); ContentProviderOperation op_email = ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) // 将第0个操作的id，即 raw_contacts 的 id 作为 data 表中的 raw_contact_id .withValueBackReference(Contacts.Data.RAW_CONTACT_ID, 0) .withValue(Contacts.Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE) .withValue(Contacts.Data.DATA1, contact.email) .withValue(Contacts.Data.DATA2, CommonDataKinds.Email.TYPE_WORK) .build(); ArrayList<ContentProviderOperation> operations = new ArrayList<>(); operations.add(op_main); operations.add(op_name); operations.add(op_phone); operations.add(op_email); try { contentResolver.applyBatch(ContactsContract.AUTHORITY,operations); } catch (OperationApplicationException | RemoteException e) { throw new RuntimeException(e); } } private void addContacts(ContentResolver resolver, Contact contact) { ContentValues values=new ContentValues(); Uri uri=resolver.insert(ContactsContract.RawContacts.CONTENT_URI,values); //获取RawContacts表中的ID long rawContactId= ContentUris.parseId(uri); ContentValues name=new ContentValues(); //关联联系人编号 name.put(Contacts.Data.RAW_CONTACT_ID,rawContactId); //姓名的数据类型 name.put(Contacts.Data.MIMETYPE,CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); //联系人姓名 name.put(Contacts.Data.DATA2,contact.name); resolver.insert(ContactsContract.Data.CONTENT_URI,name); ContentValues phone=new ContentValues(); phone.put(Contacts.Data.RAW_CONTACT_ID,rawContactId); phone.put(Contacts.Data.MIMETYPE,CommonDataKinds.Phone.CONTENT_ITEM_TYPE); phone.put(Contacts.Data.DATA1,contact.phone); //联系类型,1表示家庭,2表示工作 phone.put(Contacts.Data.DATA2,CommonDataKinds.Phone.TYPE_MOBILE); resolver.insert(ContactsContract.Data.CONTENT_URI, phone); ContentValues email=new ContentValues(); email.put(Contacts.Data.RAW_CONTACT_ID,rawContactId); email.put(Contacts.Data.MIMETYPE,CommonDataKinds.Email.CONTENT_ITEM_TYPE); email.put(Contacts.Data.DATA1,contact.email); email.put(Contacts.Data.DATA2,CommonDataKinds.Email.TYPE_WORK); resolver.insert(ContactsContract.Data.CONTENT_URI,email); } } 监听短信内容 ContentObserver监听短信 内容观察期ContentObserver给目标内容注册一个观察器，目标内容一旦发生变化，规定好 的动作马上触发，从而执行开发者预先定义的代码 例子： package com.example.client_provider; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.Log; public class MonitorsmsActivity extends AppCompatActivity { private SmsGetObserver mObserver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_monitorsms); Uri uri=Uri.parse("content://sms"); mObserver = new SmsGetObserver(this); //true表示会同时匹配uri的派生uri getContentResolver().registerContentObserver(uri,true,mObserver); } @Override protected void onDestroy() { super.onDestroy(); getContentResolver().unregisterContentObserver(mObserver); } private static class SmsGetObserver extends ContentObserver { private final Context mContext; public SmsGetObserver(Context context) { super(new Handler(Looper.getMainLooper())); this.mContext = context; } @SuppressLint("Range") @Override public void onChange(boolean selfChange, @Nullable Uri uri) { super.onChange(selfChange, uri); // onChange会多次调用，收到一条短信会调用两次onChange // mUri===content://sms/raw/20 // mUri===content://sms/inbox/20 // 安卓7.0以上系统，点击标记为已读，也会调用一次 // mUri===content://sms // 收到一条短信都是uri后面都会有确定的一个数字，对应数据库的_id，比如上面的20 if (uri == null) { return; } if (uri.toString().contains("content://sms/raw") || uri.toString().equals("content://sms")) { return; } // 通过内容解析器获取符合条件的结果集游标 Cursor cursor = mContext.getContentResolver().query(uri, new String[]{"address", "body", "date"}, null, null, "date DESC"); if (cursor.moveToNext()) { // 短信的发送号码 String sender = cursor.getString(cursor.getColumnIndex("address")); // 短信内容 String content = cursor.getString(cursor.getColumnIndex("body")); Log.d("监听到的短信:", String.format("发送方:%s,内容为:%s", sender, content)); } cursor.close(); } } } 发送彩信 通过Intent和 registerForActivityResult跳转到系统相册选择图片后返回图片到附件，再通过Intent设置权限发送彩信。 package com.example.client_provider; import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.Toast; import com.example.client_provider.util.ToastUtil; public class SendMmsActivity extends AppCompatActivity implements View.OnClickListener { private ImageView iv_appendix; private ActivityResultLauncher<Intent> mResultLauncher; private EditText et_phone; private EditText et_title; private EditText et_content; private Button btn_send; private Uri picUri; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_send_mms); iv_appendix=findViewById(R.id.iv_appenddix); iv_appendix.setOnClickListener(this); et_phone=findViewById(R.id.phone); et_title=findViewById(R.id.title); et_content=findViewById(R.id.content); btn_send=findViewById(R.id.btn_send_sms); btn_send.setOnClickListener(this); mResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { if (result.getResultCode() == RESULT_OK) { Intent intent = result.getData(); picUri = intent.getData(); if (picUri != null) { iv_appendix.setImageURI(picUri); Log.d("选中的图片是", picUri.toString()); } } } }); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.iv_appenddix: Intent intent=new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); mResultLauncher.launch(intent); break; case R.id.btn_send_sms: sendMms(et_phone.getText().toString(),et_title.getText().toString(), et_content.getText().toString()); break; } } private void sendMms(String phone,String title,String message){ Intent intent=new Intent(Intent.ACTION_SEND); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // Intent 的接受者将被准许读取Intent 携带的URI数据 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.putExtra("address",phone); intent.putExtra("title",title); intent.putExtra("message",message); intent.putExtra(Intent.EXTRA_STREAM,picUri); intent.setType("image/*"); startActivity(intent); ToastUtil.show(this,"请在弹窗中选择短信或者信息应用"); } } 通过MediaStore查询图片 需求如下，发送彩信，从手机中查询6张合适的图片显示到屏幕中供用户选择，用户点击图片即可发送彩信，需通过FileProvider，将文件暴露给第三方，并授予读写权限。 <?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="external_storage_download" path="Download"/> </paths> <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <queries> <provider android:authorities="com.example.server_provider.provider.UserInfoProvider" /> </queries> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.READ_SMS" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.MyApplication"> <activity android:name=".ProviderMmsAcivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <provider android:authorities="@string/file_provider" android:name="androidx.core.content.FileProvider" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/> </provider> </application> </manifest> package com.example.client_provider; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; import android.util.Log; import android.view.ViewGroup; import android.widget.EditText; import android.widget.GridLayout; import android.widget.ImageView; import com.example.client_provider.entity.ImageInfo; import com.example.client_provider.util.FileUtil; import com.example.client_provider.util.PermissionUtil; import com.example.client_provider.util.ToastUtil; import com.example.client_provider.util.Utils; import java.io.File; import java.util.ArrayList; import java.util.List; public class ProviderMmsAcivity extends AppCompatActivity { private static final String[] PERMISSIONS = new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }; private static final int PERMISSION_REQUEST_CODE = 1; private final List<ImageInfo> imageInfoList=new ArrayList<>(); private GridLayout gl_appendix; private EditText et_phone; private EditText et_title; private EditText et_content; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_provider_mms_acivity); gl_appendix=findViewById(R.id.gl_appendix); et_phone=findViewById(R.id.phone); et_title=findViewById(R.id.title); et_content=findViewById(R.id.content); MediaScannerConnection.scanFile(this,new String[]{Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()},null,null); if(PermissionUtil.CheckPermissions(this,PERMISSIONS,PERMISSION_REQUEST_CODE)){ loadImagelist(); showImageGrid(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if(requestCode==PERMISSION_REQUEST_CODE && PermissionUtil.checkGrant(grantResults)){ loadImagelist(); showImageGrid(); } } private void showImageGrid() { gl_appendix.removeAllViews(); for (ImageInfo info:imageInfoList){ ImageView iv_appendix=new ImageView(this); Bitmap bitmap= BitmapFactory.decodeFile(info.path); iv_appendix.setImageBitmap(bitmap); iv_appendix.setScaleType(ImageView.ScaleType.FIT_CENTER); int px= Utils.dip2px(this,110); ViewGroup.LayoutParams params=new ViewGroup.LayoutParams(px,px); iv_appendix.setLayoutParams(params); int padding=Utils.dip2px(this,5); iv_appendix.setPadding(padding,padding,padding,padding); iv_appendix.setOnClickListener(v->{ sendMms(et_phone.getText().toString(),et_title.getText().toString(), et_content.getText().toString(),info.path); }); gl_appendix.addView(iv_appendix); } } @SuppressLint("Range") private void loadImagelist() { String[] columns=new String[]{ MediaStore.Images.Media._ID, MediaStore.Images.Media.TITLE, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DATA, }; //根据Media获取图片的信息 @SuppressLint("Recycle") Cursor cursor=getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, "_size< 409600", null, "_size DESC" ); int count=0; if(cursor!=null) { while(cursor.moveToNext() && count < 6) { ImageInfo info = new ImageInfo(); info.id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID)); info.name = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.TITLE)); info.size = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.SIZE)); info.path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); //检查文件是否合法,是否允许访问。 if(FileUtil.checkFileUri(this,info.path)){ count++; imageInfoList.add(info); } Log.d("获取的图片有",info.toString()); } } } @SuppressLint("StringFormatInvalid") private void sendMms(String phone, String title, String message, String path){ Uri uri; uri= FileProvider.getUriForFile(this,getString(R.string.file_provider),new File(path)); Intent intent=new Intent(Intent.ACTION_SEND); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // Intent 的接受者将被准许读取Intent 携带的URI数据 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.putExtra("address",phone); intent.putExtra("title",title); intent.putExtra("message",message); intent.putExtra(Intent.EXTRA_STREAM,uri); intent.setType("image/*"); startActivity(intent); ToastUtil.show(this,"请在弹窗中选择短信或者信息应用"); } } 应用安装 要使用应用安装也要通过FileProvider提供,其中安卓11之后要额外获取权限android.permission.MANAGE_EXTERNAL_STORAGE的权限，安卓8的时候 要获取READ_EXTERNAL_STORAGE权限，因此这里要注意版本与权限对应。 例子：点击Install按钮自动安卓指定的应用 package com.example.client_provider; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; import android.Manifest; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.Settings; import android.view.View; import android.widget.Toast; import com.example.client_provider.util.PermissionUtil; import com.example.client_provider.util.ToastUtil; import java.io.File; public class ProviderApkActivity extends AppCompatActivity implements View.OnClickListener { //安卓8以后要获取的权限 private static final String[] PERMISSIONS=new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }; private static final int PERMISSION_REQUEST_CODE=1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_provider_apk); findViewById(R.id.install).setOnClickListener(this); } @Override public void onClick(View view) { //安卓11后要获取MANAGE_EXTERNAL_STORAGE权限 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.R){ checkAndInstall(); } else { if (PermissionUtil.CheckPermissions(this, PERMISSIONS, PERMISSION_REQUEST_CODE)) { installApk(); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if(requestCode==PERMISSION_REQUEST_CODE&& PermissionUtil.checkGrant(grantResults)){ installApk(); } } @RequiresApi(api = Build.VERSION_CODES.R) private void checkAndInstall() { if(!Environment.isExternalStorageManager()){ Intent intent=new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setData(Uri.fromParts("package",getPackageName(),null)); startActivity(intent); } else{ installApk(); } } private void installApk() { String apkPath= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()+"app-release.apk"; //应用包管理器 PackageManager packageManager=getPackageManager(); //获取apk的文件信息 PackageInfo info=packageManager.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES); if(info==null){ ToastUtil.show(this,"安装文件损坏"); return; } Uri uri; uri=FileProvider.getUriForFile(this,getString(R.string.file_provider),new File(apkPath)); Intent intent=new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(uri,"application/vnd.android.package-archive"); startActivity(intent); } } 十三、高级控件 关于高级控件的一些视图处理，会有一个总处理器Adapter，它的继承结构如下： 1. 下拉列表Spinner 下拉列表有两种模式，分别为dropdown和dialog模式即弹出框，设置下拉列表的时候，直接设置子视图后转交给ArrayAdapter处理即可， ArrayAdapter是最简单的适配器，只能显示一行文本。 例子：设置城市下拉框，监听选择的城市 package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Spinner; import com.example.highcontrolleractivity.util.ToastUtil; public class SpinnerDropdownActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { private static final String[] CityArray=new String[]{"广州","上海","北京","深圳","杭州"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_spinner_dropdown); Spinner sp_dropdown=findViewById(R.id.sp_dropdown); ArrayAdapter<String> stringArrayAdapter=new ArrayAdapter<>(this,R.layout.item_selector,CityArray); sp_dropdown.setAdapter(stringArrayAdapter); sp_dropdown.setSelection(0); sp_dropdown.setOnItemSelectedListener(this); } @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { ToastUtil.show(this,"你选择的是"+CityArray[i]); } @Override public void onNothingSelected(AdapterView<?> adapterView) { } } 2.简单适配器 因为数组适配器只能显示一行文本，因此可以使用简单适配器，同时放置图片和文本，可以使用简单适配器 package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.SimpleAdapter; import android.widget.Spinner; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class SpinnerIconActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { private static final int[] iconArray = { R.drawable.shuixing, R.drawable.jinxing, R.drawable.diqiu, R.drawable.huoxing, R.drawable.muxing, R.drawable.tuxing }; private static final String[] starArray = {"水星", "金星", "地球", "火星", "木星", "土星"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_spinner_icon); Spinner sp_icon=findViewById(R.id.sp_icon); List<Map<String,Object>> list=new ArrayList<>(); for (int i = 0; i < iconArray.length; i++) { Map<String,Object> stringObjectMap=new HashMap<>(); stringObjectMap.put("icon",iconArray[i]); stringObjectMap.put("name",starArray[i]); list.add(stringObjectMap); } SimpleAdapter simpleAdapter=new SimpleAdapter(this,list, R.layout.item_simple, new String[]{"icon","name"}, new int[]{R.id.iv_icon,R.id.tv_name}); sp_icon.setAdapter(simpleAdapter); sp_icon.setSelection(0); sp_icon.setOnItemSelectedListener(this); } @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { ToastUtil.show(this, "您选择的是" + starArray[i]); } @Override public void onNothingSelected(AdapterView<?> adapterView) { } } 3. BaseAdapter 假如现在需要设置的下拉列表为，左边是图片，右边是星球名字，右下角要附上简述，就得使用BaseAdapter这种更上层的Adapter适配器，BaseAdapter是一个抽象方法， 主要通过重写getView()方法设置下拉列表的视图信息，因此BaseAdapter拥有更高的适应性。 例子： PlaneBaseAdapter类： package com.example.highcontrolleractivity.adapter; import android.annotation.SuppressLint; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.entity.Planet; import java.util.List; public class PlaneBaseAdapter extends BaseAdapter { private Context context; private List<Planet> planetList; public PlaneBaseAdapter(Context context, List<Planet> planetList) { this.context = context; this.planetList = planetList; } @Override public int getCount() { return planetList.size(); } @Override public Object getItem(int i) { return planetList.get(i); } @Override public long getItemId(int i) { return i; } @Override public View getView(int i, View view, ViewGroup viewGroup) { @SuppressLint("ViewHolder") View view1= LayoutInflater.from(context).inflate(R.layout.item_list,null); ImageView imageView=view1.findViewById(R.id.iv_icon); TextView tv_name=view1.findViewById(R.id.tv_name); TextView tv_desc=view1.findViewById(R.id.tv_desc); Planet planet=planetList.get(i); imageView.setImageResource(planet.image); tv_name.setText(planet.name); tv_desc.setText(planet.desc); return view1; } } Activity类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.Spinner; import com.example.highcontrolleractivity.adapter.PlaneBaseAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.List; public class BaseAdapterAcivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { private List<Planet> planetList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_base_adapter_acivity); Spinner sp_planet = findViewById(R.id.sp_baseAdapter); planetList = Planet.getDefaultList(); // 构建一个行星列表的适配器 PlaneBaseAdapter adapter = new PlaneBaseAdapter(this, planetList); sp_planet.setAdapter(adapter); sp_planet.setSelection(0); sp_planet.setOnItemSelectedListener(this); } @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { ToastUtil.show(this, "您选择的是" + planetList.get(position).name); } @Override public void onNothingSelected(AdapterView<?> parent) { } } plane类： package com.example.highcontrolleractivity.entity; import com.example.highcontrolleractivity.R; import java.util.ArrayList; import java.util.List; public class Planet { public int image; // 行星图标 public String name; // 行星名称 public String desc; // 行星描述 public Planet(int image, String name, String desc) { this.image = image; this.name = name; this.desc = desc; } private static int[] iconArray = {R.drawable.shuixing, R.drawable.jinxing, R.drawable.diqiu, R.drawable.huoxing, R.drawable.muxing, R.drawable.tuxing}; private static String[] nameArray = {"水星", "金星", "地球", "火星", "木星", "土星"}; private static String[] descArray = { "水星是太阳系八大行星最内侧也是最小的一颗行星，也是离太阳最近的行星", "金星是太阳系八大行星之一，排行第二，距离太阳0.725天文单位", "地球是太阳系八大行星之一，排行第三，也是太阳系中直径、质量和密度最大的类地行星，距离太阳1.5亿公里", "火星是太阳系八大行星之一，排行第四，属于类地行星，直径约为地球的53%", "木星是太阳系八大行星中体积最大、自转最快的行星，排行第五。它的质量为太阳的千分之一，但为太阳系中其它七大行星质量总和的2.5倍", "土星为太阳系八大行星之一，排行第六，体积仅次于木星" }; public static List<Planet> getDefaultList() { List<Planet> planetList = new ArrayList<Planet>(); for (int i = 0; i < iconArray.length; i++) { planetList.add(new Planet(iconArray[i], nameArray[i], descArray[i])); } return planetList; } } ConvertView复用 即使是下拉列表，在屏幕中展示的条数也是有限的，因此需要复用convertview使得屏幕能够通过拉动的形式展示更多的条数。 对上面做出优化： @Override public View getView(int i, View view, ViewGroup viewGroup) { //当一个屏幕显示的条数构建完,再需要构建就会走else那部分,复用并刷新数据 ViewHolder viewHolder; if(view==null) { view = LayoutInflater.from(context).inflate(R.layout.item_list, null); viewHolder= new ViewHolder(); viewHolder.iv_icon = view.findViewById(R.id.iv_icon); viewHolder.tv_name = view.findViewById(R.id.tv_name); viewHolder.tv_desc = view.findViewById(R.id.tv_desc); view.setTag(viewHolder); } else { viewHolder=(ViewHolder) view.getTag(); } Planet planet=planetList.get(i); viewHolder.iv_icon.setImageResource(planet.image); viewHolder.tv_name.setText(planet.name); viewHolder.tv_desc.setText(planet.desc); return view; } public static final class ViewHolder{ public ImageView iv_icon; public TextView tv_name; public TextView tv_desc; } 4.列表视图listView ListView是一个显示滚动项列表的示视图组(viewgroup)，通过使用适配器(Adapter)把这些列表项自动插入到列表中。适配器比如从一个数组或是数据库查询获取到数据，然后转换每一项成为可放入到列表的视图，一般也是联合BaseAdapter一起使用，要注意的一点是当条目事件里面存在Button等点击事件的时候，会出现条目事件冲突，因为条目点击事件会交给子容器处理，因此不生效，要在子条目视图中使用descendantFocusability="blocksDescendants">阻止下级控件的获取，解决冲突问题。 例子：设置两个checkbox，分别设置是否显示列表视图的分割线和按压背景，按压背景默认是灰色，可自定义。 package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ListView; import com.example.highcontrolleractivity.adapter.PlaneBaseAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.util.ToastUtil; import com.example.highcontrolleractivity.util.Utils; import java.util.List; public class ListViewActivity extends AppCompatActivity implements AdapterView.OnItemClickListener, CompoundButton.OnCheckedChangeListener { private ListView iv_planet; private List<Planet> planetList; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_view); iv_planet=findViewById(R.id.iv_planet); planetList=Planet.getDefaultList(); PlaneBaseAdapter planeBaseAdapter=new PlaneBaseAdapter(this,planetList); iv_planet.setAdapter(planeBaseAdapter); iv_planet.setOnItemClickListener(this); CheckBox ck_divider=findViewById(R.id.ck_divider); CheckBox ck_selector=findViewById(R.id.ck_selector); ck_divider.setOnCheckedChangeListener(this); ck_selector.setOnCheckedChangeListener(this); } @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { ToastUtil.show(this,"您选择的星球是"+planetList.get(i).name); } @SuppressLint("NonConstantResourceId") @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { switch (compoundButton.getId()){ case R.id.ck_divider: if(compoundButton.isChecked()){ @SuppressLint("UseCompatLoadingForDrawables") Drawable divider=getResources().getDrawable(R.color.black,getTheme()); iv_planet.setDivider(divider); iv_planet.setDividerHeight(Utils.dip2px(this,1)); } else{ iv_planet.setDivider(null); iv_planet.setDividerHeight(0); } break; case R.id.ck_selector: if(compoundButton.isChecked()){ @SuppressLint("UseCompatLoadingForDrawables") Drawable selector=getResources().getDrawable(R.drawable.list_selector,getTheme()); iv_planet.setSelector(selector); } else{ @SuppressLint("UseCompatLoadingForDrawables") Drawable selector=getResources().getDrawable(R.color.transparent,getTheme()); iv_planet.setSelector(selector); } break; } } } 5.网格视图GirdView GirdView的属性： XML中的属性 GirdView类中的设置方法 说明 horizontalSpace setHorizontalSpacing 指定网格项在水平方向的间距 verticalSpacing setVerticalSpacing 指定网格项在垂直方向的间距 numColumns setNumColumns 指定列数目 stretchMode setStretchMode 指定剩余空间的拉伸模式 columnWidth setColumnWidth 指定每列宽度 关于stretchMode的设置： XML中的属性 GirdView类中的设置方法 说明 none NO_STRETCH 不拉伸 columnWidth STRETCH_COLUMN_WIDTH 若有剩余空间，拉伸列宽挤掉空隙 spacingWidth STRETCH_SPACING 若有剩余空间，则列宽不变，把空间分配到每列间的空隙 spacingWidthUniform STRETCH_SPACING_UNIFORM 若有剩余空间，列宽不变，把空间分配到列作业间隙 例子：还是星球的例子 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".GirdViewActivity" android:orientation="vertical"> <GridView android:id="@+id/gv_planet" android:layout_width="match_parent" android:layout_height="wrap_content" android:numColumns="2" android:verticalSpacing="3dp" android:horizontalSpacing="3dp" android:columnWidth="100dp" android:stretchMode="columnWidth"/> </LinearLayout> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:padding="5dp"> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="wrap_content" tools:text="地球" android:textSize="17sp" android:textColor="@color/black" android:gravity="center"/> <ImageView android:id="@+id/iv_icon" android:layout_width="match_parent" android:layout_height="100dp" android:scaleType="fitCenter" tools:src="@drawable/diqiu"/> <TextView android:id="@+id/tv_desc" android:layout_width="match_parent" android:layout_height="95dp" android:textSize="13sp" android:textColor="@color/black" android:gravity="start|top" tools:text="木星是太阳系八大行星中体积最大、自转最快的行星，排行第五。它的质量为太阳的千分之一，但为太阳系中其它七大行星质量总和的2.5倍。"/> </LinearLayout> package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.GridView; import com.example.highcontrolleractivity.adapter.PlaneBaseWithButtonAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.List; public class GirdViewActivity extends AppCompatActivity implements AdapterView.OnItemClickListener { private GridView gridView; private List<Planet> planetList; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gird_view); gridView = findViewById(R.id.gv_planet); planetList = Planet.getDefaultList(); PlaneBaseWithButtonAdapter planeBaseWithButtonAdapter=new PlaneBaseWithButtonAdapter(this, planetList); gridView.setAdapter(planeBaseWithButtonAdapter); gridView.setOnItemClickListener(this); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { ToastUtil.show(this, "您选择了：" + planetList.get(position).name); } } 6.翻页视图ViewPager 同样，需要从PagetAdapter类中继承，然后重写类的方法。 例子：对星球的视图进行翻页处理，并在上方启用标签栏。 Adapater类： package com.example.highcontrolleractivity.adapter; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import com.example.highcontrolleractivity.ViewPagerActivity; import com.example.highcontrolleractivity.entity.Planet; import java.util.ArrayList; import java.util.List; public class ImagePagerAdapter extends PagerAdapter { private final ViewPagerActivity context; private final List<Planet> planetList; private final List<ImageView> ViewList=new ArrayList<>(); public ImagePagerAdapter(ViewPagerActivity context, List<Planet> planetList) { this.context=context; this.planetList=planetList; for (Planet planet : planetList) { ImageView view=new ImageView(context); view.setImageResource(planet.image); view.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT )); ViewList.add(view); } } @Override public int getCount() { return ViewList.size(); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view==object; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { //添加view到container,后返回一个与view关联的对象,这里直接返回自身,与isViewFromObject方法中的object相等 ImageView view=ViewList.get(position); container.addView(view); return view; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView(ViewList.get(position)); } //返回文字到标签栏 @Nullable @Override public CharSequence getPageTitle(int position) { return planetList.get(position).name; } } Activity类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerTabStrip; import androidx.viewpager.widget.ViewPager; import android.graphics.Color; import android.os.Bundle; import android.util.TypedValue; import com.example.highcontrolleractivity.adapter.ImagePagerAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.ArrayList; import java.util.List; public class ViewPagerActivity extends AppCompatActivity implements ViewPager.OnPageChangeListener { private ViewPager vp_content; private List<Planet> planetList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_view_pager); initPagerStrip(); initViewPager(); } //初始化翻页视图 private void initViewPager() { vp_content = findViewById(R.id.vp_content); planetList = Planet.getDefaultList(); PagerAdapter pagerAdapter= new ImagePagerAdapter(this, planetList); vp_content.setAdapter(pagerAdapter); vp_content.addOnPageChangeListener(this); vp_content.setCurrentItem(2); } //初始化翻页标签栏 private void initPagerStrip() { PagerTabStrip pagerTabStrip=findViewById(R.id.pts_tab); //此包源于androidx,相当于android的扩展,因此能够直接使用sp,不需要自己转换 pagerTabStrip.setTextSize(TypedValue.COMPLEX_UNIT_SP,20); pagerTabStrip.setTextColor(Color.BLACK); } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { ToastUtil.show(this, "您翻到的手机品牌是：" + planetList.get(position).name); } @Override public void onPageScrollStateChanged(int state) { } } 7.ViewPager设置引导页 一般进入程序后会现出现引导页，引导页面进入，原理其实就是通过Viewpager翻页的形式制作引导页，通过PageAdapter嵌入view视图。 引导页的视图： <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_launch" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitXY"/> <RadioGroup android:id="@+id/rg_indicate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:paddingBottom="20dp" android:orientation="horizontal" /> </RelativeLayout> Activity视图： <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".SimpleLaunchActivity"> <androidx.viewpager.widget.ViewPager android:id="@+id/vp_launch" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout> Adapter类： package com.example.highcontrolleractivity.adapter; import android.os.Build; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.RadioButton; import android.widget.RadioGroup; import androidx.annotation.NonNull; import androidx.viewpager.widget.PagerAdapter; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.SimpleLaunchActivity; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.ArrayList; import java.util.List; public class SimpleLaunchAdapter extends PagerAdapter { private final List<View> viewList=new ArrayList<>(); public SimpleLaunchAdapter(SimpleLaunchActivity context, int[] arrayPic) { for (int i = 0; i < arrayPic.length; i++) { View view= LayoutInflater.from(context).inflate(R.layout.item_launch,null); ImageView iv_launch=view.findViewById(R.id.iv_launch); RadioGroup rg_indicate=view.findViewById(R.id.rg_indicate); Button btn_start=view.findViewById(R.id.btn_start); iv_launch.setImageResource(arrayPic[i]); //每个页面的RadioGroup要有四个RadioButton for (int j = 0; j < arrayPic.length; j++) { RadioButton radioButton=new RadioButton(context); radioButton.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT )); radioButton.setPadding(10,10,10,10); rg_indicate.addView(radioButton); } //当前位置的单选按钮要高亮显示 ((RadioButton)rg_indicate.getChildAt(i)).setChecked(true); //最后一个引导页则开启开关 if(i==arrayPic.length-1){ btn_start.setVisibility(View.VISIBLE); btn_start.setOnClickListener(v->{ ToastUtil.show(context,"开启美好生活,跳入应用程序主页面"); }); } viewList.add(view); } } @Override public int getCount() { return viewList.size(); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view==object; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { View view=viewList.get(position); container.addView(view); return view; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView(viewList.get(position)); } } Activity类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import androidx.viewpager.widget.ViewPager; import android.annotation.SuppressLint; import android.os.Bundle; import com.example.highcontrolleractivity.adapter.SimpleLaunchAdapter; public class SimpleLaunchActivity extends AppCompatActivity { private final int[] ArrayPic=new int[]{ R.drawable.guide_bg1, R.drawable.guide_bg2, R.drawable.guide_bg3, R.drawable.guide_bg4 }; @SuppressLint({"MissingInflatedId", "LocalSuppress"}) private ViewPager vp_launch; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_simple_launch); vp_launch = findViewById(R.id.vp_launch); SimpleLaunchAdapter launchAdapter=new SimpleLaunchAdapter(this,ArrayPic); vp_launch.setAdapter(launchAdapter); } } 8.碎片Fragment 传统的Activity不能很好的处理大屏问题,急需碎片化的东西能够划区域展示内容，且有属于自己的独立可操作空间，因此诞生了Fragment，Fragment注册方式分为动态注册和静态注册： 静态注册：直接在布局文件中直接指定Fragment，动态注册：直接在代码中动态添加Fragment。 Fragment生命周期：onAttach->onCreate->OnCreateView->onViewCreate->onStart->onResume->onPause->onStop->onDestroyView->onDestory->onDetach 静态注册 当加载layout的fragment页面时，会自动加载指向的fragment类，进而加载fragment页面 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <fragment android:layout_width="match_parent" android:layout_height="60dp" android:id="@+id/fragment_static" android:name="com.example.highcontrolleractivity.fragment.StaticFragment"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="17sp" android:textColor="@color/black" android:gravity="center" android:text="fragment展示页"/> </LinearLayout> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ddffdd" android:padding="5dp" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="广告图片" android:textColor="@color/black" android:textSize="17sp"/> <ImageView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="4" android:src="@drawable/adv" android:scaleType="fitCenter"/> </LinearLayout> 动态注册 同样需要从FragmentAdapter继承类，重写此类承载ViewPager，与之前有所不同的是getItem需要返回的是Fragment类，所以需要重写Fragment方法， 利用onCreateView创建视图效果。 例子：之前星球的例子 主Activity类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerTabStrip; import androidx.viewpager.widget.ViewPager; import android.graphics.Color; import android.os.Bundle; import android.util.TypedValue; import com.example.highcontrolleractivity.adapter.DynamicFragmentAdapter; import com.example.highcontrolleractivity.entity.Planet; import java.util.List; public class FragmentDynamicActivity extends AppCompatActivity { private ViewPager vp_content; private List<Planet> planetList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_fragment_dynamic); initPagerStrip(); initViewPager(); } private void initViewPager() { vp_content = findViewById(R.id.vp_content); planetList = Planet.getDefaultList(); DynamicFragmentAdapter dynamicFragmentAdapter=new DynamicFragmentAdapter(getSupportFragmentManager(),planetList); vp_content.setAdapter(dynamicFragmentAdapter); vp_content.setCurrentItem(2); } private void initPagerStrip() { PagerTabStrip pagerTabStrip=findViewById(R.id.pts_tab); //此包源于androidx,相当于android的扩展,因此能够直接使用sp,不需要自己转换 pagerTabStrip.setTextSize(TypedValue.COMPLEX_UNIT_SP,20); pagerTabStrip.setTextColor(Color.BLACK); } } FragmentAdapter类： package com.example.highcontrolleractivity.adapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.fragment.DynamicFragment; import java.util.List; public class DynamicFragmentAdapter extends FragmentPagerAdapter { private List<Planet> planetList; public DynamicFragmentAdapter(@NonNull FragmentManager fm, List<Planet> planetList) { //将当前的fragment设置为resume状态，上一个fragment设置为start //从而通过fragment的onResume()来懒加载数据 super(fm,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.planetList=planetList; } @NonNull @Override public Fragment getItem(int position) { Planet planet=planetList.get(position); return DynamicFragment.newInstance(position,planet.image,planet.desc); } @Override public int getCount() { return planetList.size(); } @Nullable @Override public CharSequence getPageTitle(int position) { return planetList.get(position).name; } } DynamicFragment类： package com.example.highcontrolleractivity.adapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import com.example.highcontrolleractivity.entity.Planet; import com.example.highcontrolleractivity.fragment.DynamicFragment; import java.util.List; public class DynamicFragmentAdapter extends FragmentPagerAdapter { private List<Planet> planetList; public DynamicFragmentAdapter(@NonNull FragmentManager fm, List<Planet> planetList) { //将当前的fragment设置为resume状态，上一个fragment设置为start //从而通过fragment的onResume()来懒加载数据 super(fm,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.planetList=planetList; } @NonNull @Override public Fragment getItem(int position) { Planet planet=planetList.get(position); return DynamicFragment.newInstance(position,planet.image,planet.desc); } @Override public int getCount() { return planetList.size(); } @Nullable @Override public CharSequence getPageTitle(int position) { return planetList.get(position).name; } } 主Activity的xml与之前Viewpager相同，Fragment的xml与之前地球子视图相同。 使用Fragment提高引导页的性能 之前的引导页，事实上就是将全部页面都存到了内存之中，每切换一个页面则从内存中提取，一旦页面很多，性能就十分不好， 通过Fragment搭配ViewPager，一旦发生页面切换，则相邻页面会被加载，非相邻页面会被回收，提高了启动速度，也不会造成资源的浪费。 主要Fragment代码如下，其它代码与上方例子相似： package com.example.highcontrolleractivity.fragment; import android.content.Context; import android.os.Bundle; import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.RadioButton; import android.widget.RadioGroup; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.util.ToastUtil; public class LaunchFragment extends Fragment { public static LaunchFragment newInstance(int count,int position,int image) { LaunchFragment fragment = new LaunchFragment(); Bundle args = new Bundle(); args.putInt("count",count); args.putInt("position",position); args.putInt("image",image); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Bundle bundle=getArguments(); Context context=getContext(); assert bundle != null; int count=bundle.getInt("count"); int image=bundle.getInt("image"); int position=bundle.getInt("position"); View view= LayoutInflater.from(context).inflate(R.layout.fragment_launch,container,false); ImageView iv_launch=view.findViewById(R.id.iv_launch); RadioGroup rg_indicate=view.findViewById(R.id.rg_indicate); Button btn_start=view.findViewById(R.id.btn_start); iv_launch.setImageResource(image); //每个页面的RadioGroup要有四个RadioButton for (int j = 0; j < count; j++) { RadioButton radioButton=new RadioButton(context); radioButton.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT )); radioButton.setPadding(10,10,10,10); rg_indicate.addView(radioButton); } //当前位置的单选按钮要高亮显示 ((RadioButton)rg_indicate.getChildAt(position)).setChecked(true); //最后一个引导页则开启开关 if(position==count-1){ btn_start.setVisibility(View.VISIBLE); btn_start.setOnClickListener(v->{ ToastUtil.show(context,"开启美好生活,跳入应用程序主页面"); }); } return view; } } 9.综合训练(记账本功能) 实现的功能： 记账功能，能够选择日期，选择收入或支出，填入事项说明，金额插入数据库 查询功能，能够根据根据，展示出当月的所有支出和收入 数据库设计： 账单表：id，date(日期)，type(支出、收入)，amount(金额)，remark(说明) 功能实现： 通过OpenDBHelper初始化数据库，并通过页面设置添加进入数据库，查询功能使用ViewPager+ListView实现，通过ViewPager+Fragment的形式 承载ListView进行实现。 实现如下： 添加账单bill_add页面： <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".BillPagerActivity"> <include layout="@layout/title_booking"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center|top" android:orientation="vertical" android:paddingLeft="5dp" android:paddingRight="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="40dp" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center|right" android:text="请选择月份：" android:textColor="@color/black" android:textSize="17sp" /> <TextView android:id="@+id/tv_month" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:drawableRight="@drawable/arrow_down" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="2035-05"/> </LinearLayout> <androidx.viewpager.widget.ViewPager android:id="@+id/vp_bill" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.viewpager.widget.PagerTabStrip android:id="@+id/pts_bill" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </androidx.viewpager.widget.ViewPager> </LinearLayout> </LinearLayout> bill_pager页面： <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".BillPagerActivity"> <include layout="@layout/title_booking"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center|top" android:orientation="vertical" android:paddingLeft="5dp" android:paddingRight="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="40dp" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center|right" android:text="请选择月份：" android:textColor="@color/black" android:textSize="17sp" /> <TextView android:id="@+id/tv_month" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:drawableRight="@drawable/arrow_down" android:gravity="center" android:textColor="@color/black" android:textSize="17sp" tools:text="2035-05"/> </LinearLayout> <androidx.viewpager.widget.ViewPager android:id="@+id/vp_bill" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.viewpager.widget.PagerTabStrip android:id="@+id/pts_bill" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </androidx.viewpager.widget.ViewPager> </LinearLayout> </LinearLayout> item_bill页面: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <TextView android:id="@+id/tv_date" android:layout_width="0dp" android:layout_height="45dp" android:layout_weight="1" android:gravity="center" android:textColor="@color/black" android:textSize="16sp" tools:text="2035-09" /> <TextView android:id="@+id/tv_remark" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:gravity="center" android:textColor="@color/black" android:textSize="16sp" tools:text="foods" /> <TextView android:id="@+id/tv_amount" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center" android:textColor="@color/black" android:textSize="16sp" tools:text="200" /> </LinearLayout> fragment_bill页面: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center|top" android:orientation="vertical"> <ListView android:id="@+id/lv_bill" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> title_booking页面： <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="50dp" xmlns:tools="http://schemas.android.com/tools" android:background="#aaaaff"> <ImageView android:id="@+id/iv_back" android:layout_width="50dp" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:padding="5dp" android:scaleType="fitCenter" android:src="@drawable/ic_back"/> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="match_parent" android:textSize="20sp" android:textColor="@color/black" android:layout_centerInParent="true" android:gravity="center" tools:text="账单列表"/> <TextView android:id="@+id/tv_option" android:layout_width="wrap_content" android:layout_height="match_parent" android:textSize="17sp" android:textColor="@color/black" android:layout_alignParentEnd="true" android:layout_marginEnd="15dp" android:gravity="center" tools:text="添加账单"/> </RelativeLayout> BillInfo实体类： package com.example.highcontrolleractivity.entity; public class BillInfo { public int id; public String date; public int type; public double amount; public String remark; // 账单类型，0 收入，1 支出 public static final int BILL_TYPE_INCOME = 0; public static final int BILL_TYPE_COST = 1; @Override public String toString() { return "BillInfo{" + "id=" + id + ", date='" + date + '\'' + ", type=" + type + ", amount=" + amount + ", remark='" + remark + '\'' + '}'; } } BillDBHelper类： package com.example.highcontrolleractivity.database; import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import androidx.annotation.Nullable; import com.example.highcontrolleractivity.entity.BillInfo; import java.util.ArrayList; import java.util.List; public class BillDBHelper extends SQLiteOpenHelper { private static final String DB_NAME="bill.db"; private static final int DB_VERSION=1; private static BillDBHelper dbHelper=null; private static final String TABLE_BILLS_INFO = "bill_info"; private SQLiteDatabase mRDB=null; private SQLiteDatabase mWDB=null; public BillDBHelper(Context context) { super(context,DB_NAME,null,DB_VERSION); } public static BillDBHelper getInstance(Context context){ if(dbHelper==null){ dbHelper=new BillDBHelper(context); } return dbHelper; } public SQLiteDatabase openReadLink(){ if(mRDB==null || !mRDB.isOpen()){ mRDB=dbHelper.getReadableDatabase(); } return mRDB; } public SQLiteDatabase openWriteLink() { if (mWDB == null || !mWDB.isOpen()) { mWDB = dbHelper.getWritableDatabase(); } return mWDB; } public void closeLink() { if (mRDB != null && mRDB.isOpen()) { mRDB.close(); mRDB = null; } if (mWDB != null && mWDB.isOpen()) { mWDB.close(); mWDB = null; } } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String sql = "CREATE TABLE IF NOT EXISTS " + TABLE_BILLS_INFO + "(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + " date VARCHAR NOT NULL," + " type INTEGER NOT NULL," + " amount DOUBLE NOT NULL," + " remark VARCHAR NOT NULL);"; sqLiteDatabase.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } public long save(BillInfo billInfo){ ContentValues values=new ContentValues(); values.put("date",billInfo.date); values.put("type",billInfo.type); values.put("amount",billInfo.amount); values.put("remark",billInfo.remark); return mRDB.insert(TABLE_BILLS_INFO,null,values); } @SuppressLint("Range") public List<BillInfo> queryByMonth(String yearMonth){ List<BillInfo> list=new ArrayList<>(); String sql = "select * from " + TABLE_BILLS_INFO + " where date like '" + yearMonth + "%'"; @SuppressLint("Recycle") Cursor cursor=mRDB.rawQuery(sql,null); while (cursor.moveToNext()) { BillInfo bill = new BillInfo(); bill.id = cursor.getInt(cursor.getColumnIndex("_id")); bill.date = cursor.getString(cursor.getColumnIndex("date")); bill.type = cursor.getInt(cursor.getColumnIndex("type")); bill.amount = cursor.getDouble(cursor.getColumnIndex("amount")); bill.remark = cursor.getString(cursor.getColumnIndex("remark")); list.add(bill); } return list; } } BillAddActivity类： package com.example.highcontrolleractivity; import static com.example.highcontrolleractivity.R.id.btn_save; import static com.example.highcontrolleractivity.R.id.et_remark; import static com.example.highcontrolleractivity.R.id.iv_back; import static com.example.highcontrolleractivity.R.id.rg_type; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.app.DatePickerDialog; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.DatePicker; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; import com.example.highcontrolleractivity.database.BillDBHelper; import com.example.highcontrolleractivity.entity.BillInfo; import com.example.highcontrolleractivity.util.DateUtil; import com.example.highcontrolleractivity.util.ToastUtil; import java.util.Calendar; public class BillAddActivity extends AppCompatActivity implements View.OnClickListener, DatePickerDialog.OnDateSetListener { private Calendar calendar; private TextView tv_date; private BillDBHelper billDBHelper; private RadioGroup rg_type; private EditText et_remark; private EditText et_amount; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_bill_add); TextView tv_title = findViewById(R.id.tv_title); TextView tv_option=findViewById(R.id.tv_option); tv_date = findViewById(R.id.tv_date); rg_type = findViewById(R.id.rg_type); et_remark = findViewById(R.id.et_remark); et_amount = findViewById(R.id.et_amount); findViewById(btn_save).setOnClickListener(this); tv_title.setText("添加账单"); tv_option.setText("账单列表"); tv_option.setOnClickListener(this); findViewById(iv_back).setOnClickListener(this); tv_date.setOnClickListener(this); calendar = Calendar.getInstance(); String date = DateUtil.getDate(calendar); tv_date.setText(date); billDBHelper = BillDBHelper.getInstance(this); billDBHelper.openReadLink(); billDBHelper.openWriteLink(); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.tv_date: DatePickerDialog datePickerDialog=new DatePickerDialog(this,this, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); datePickerDialog.show(); break; case R.id.btn_save: BillInfo billInfo=new BillInfo(); billInfo.date=tv_date.getText().toString(); billInfo.type=rg_type.getCheckedRadioButtonId()==R.id.rb_income ? BillInfo.BILL_TYPE_INCOME : BillInfo.BILL_TYPE_COST; billInfo.remark=et_remark.getText().toString(); billInfo.amount= Double.parseDouble(et_amount.getText().toString()); if(billDBHelper.save(billInfo)>0){ ToastUtil.show(this,"保存账单成功"); } break; case R.id.tv_option: // 跳转到添加账单页面 Intent intent = new Intent(this, BillPagerActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; case R.id.iv_back: // 关闭当前页面 finish(); break; } } @Override public void onDateSet(DatePicker datePicker, int year, int month, int day) { calendar.set(Calendar.YEAR,year); calendar.set(Calendar.MONTH,month); calendar.set(Calendar.DAY_OF_MONTH,day); tv_date.setText(DateUtil.getDate(calendar)); } } BillPager类： package com.example.highcontrolleractivity; import androidx.appcompat.app.AppCompatActivity; import androidx.viewpager.widget.PagerTabStrip; import androidx.viewpager.widget.ViewPager; import android.annotation.SuppressLint; import android.app.DatePickerDialog; import android.content.Intent; import android.os.Bundle; import android.util.TypedValue; import android.view.View; import android.widget.DatePicker; import android.widget.TextView; import com.example.highcontrolleractivity.adapter.BillPagerAdapter; import com.example.highcontrolleractivity.database.BillDBHelper; import com.example.highcontrolleractivity.util.DateUtil; import java.util.Calendar; public class BillPagerActivity extends AppCompatActivity implements View.OnClickListener, DatePickerDialog.OnDateSetListener { private Calendar calendar; private TextView tv_month; private ViewPager vp_bill; private BillDBHelper mDBHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_bill_pager); TextView tv_title=findViewById(R.id.tv_title); TextView tv_option=findViewById(R.id.tv_option); tv_month = findViewById(R.id.tv_month); tv_title.setText("账单列表"); tv_option.setText("添加账单"); tv_option.setOnClickListener(this); tv_month.setOnClickListener(this); calendar = Calendar.getInstance(); tv_month.setText(DateUtil.getMonth(calendar)); findViewById(R.id.iv_back).setOnClickListener(this); mDBHelper = BillDBHelper.getInstance(this); mDBHelper.openReadLink(); mDBHelper.openWriteLink(); initViewPager(); } private void initViewPager() { PagerTabStrip pts_bill=findViewById(R.id.pts_bill); pts_bill.setTextSize(TypedValue.COMPLEX_UNIT_SP, 17); vp_bill=findViewById(R.id.vp_bill); BillPagerAdapter adapter=new BillPagerAdapter(getSupportFragmentManager(), calendar.get(Calendar.YEAR)); vp_bill.setAdapter(adapter); vp_bill.setCurrentItem(calendar.get(Calendar.MONTH)); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.tv_month: DatePickerDialog datePickerDialog=new DatePickerDialog(this,this, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); datePickerDialog.show(); break; case R.id.tv_option: // 跳转到添加账单页面 Intent intent = new Intent(this, BillAddActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; case R.id.iv_back: // 关闭当前页面 finish(); break; } } @Override public void onDateSet(DatePicker datePicker, int year, int month, int day) { calendar.set(Calendar.YEAR,year); calendar.set(Calendar.MONTH,month); calendar.set(Calendar.DAY_OF_MONTH,day); tv_month.setText(DateUtil.getMonth(calendar)); vp_bill.setCurrentItem(month); } @Override protected void onDestroy() { super.onDestroy(); mDBHelper.closeLink(); } } BillPagerAdapter类： package com.example.highcontrolleractivity.adapter; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import com.example.highcontrolleractivity.fragment.BillFragment; public class BillPagerAdapter extends FragmentPagerAdapter { private final int mYear; public BillPagerAdapter(@NonNull FragmentManager fm,int year) { super(fm,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.mYear = year; } @NonNull @Override public Fragment getItem(int position) { int month=position+1; String zeroMonth = month < 10 ? "0" + month : String.valueOf(month); String yearMonth = mYear + "-" + zeroMonth; return BillFragment.newInstance(yearMonth); } @Override public int getCount() { return 12; } @Nullable @Override public CharSequence getPageTitle(int position) { return (position+1)+"月份"; } } BillFragment类： package com.example.highcontrolleractivity.fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import androidx.fragment.app.Fragment; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.adapter.BillListAdapter; import com.example.highcontrolleractivity.database.BillDBHelper; import com.example.highcontrolleractivity.entity.BillInfo; import java.util.List; public class BillFragment extends Fragment { public static BillFragment newInstance(String yearMonth){ BillFragment fragment = new BillFragment(); Bundle args = new Bundle(); args.putString("yearMonth", yearMonth); fragment.setArguments(args); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_bill, container, false); ListView lv_bill=view.findViewById(R.id.lv_bill); BillDBHelper mDBHelper = BillDBHelper.getInstance(getContext()); Bundle arguments = getArguments(); String yearMonth = arguments.getString("yearMonth"); List<BillInfo> billInfoList = mDBHelper.queryByMonth(yearMonth); BillListAdapter adapter = new BillListAdapter(getContext(), billInfoList); lv_bill.setAdapter(adapter); return view; } } BillListAdapter类： package com.example.highcontrolleractivity.adapter; import android.annotation.SuppressLint; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import com.example.highcontrolleractivity.R; import com.example.highcontrolleractivity.entity.BillInfo; import java.util.List; public class BillListAdapter extends BaseAdapter { private final Context mContext; private final List<BillInfo> mBillList; public BillListAdapter(Context context, List<BillInfo> billInfoList) { this.mContext = context; this.mBillList = billInfoList; } @Override public int getCount() { return mBillList.size(); } @Override public Object getItem(int position) { return mBillList.get(position); } @Override public long getItemId(int position) { return mBillList.get(position).id; } @SuppressLint("DefaultLocale") @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(); convertView = LayoutInflater.from(mContext).inflate(R.layout.item_bill, null); holder.tv_date = convertView.findViewById(R.id.tv_date); holder.tv_remark = convertView.findViewById(R.id.tv_remark); holder.tv_amount = convertView.findViewById(R.id.tv_amount); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } BillInfo bill = mBillList.get(position); holder.tv_date.setText(bill.date); holder.tv_remark.setText(bill.remark); holder.tv_amount.setText(String.format("%s%d元", bill.type == 0 ? "+" : "-", (int) bill.amount)); return convertView; } public final class ViewHolder{ public TextView tv_date; public TextView tv_remark; public TextView tv_amount; } } 实现效果如下： 十五、广播 标准广播(无序广播) 收发过程分为三个步骤： 1.发送标准广播 2.定义广播接收器 3.开关广播接收器 package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import com.example.broadcast.receiver.StandardReceiver; public class BroadStandardActivity extends AppCompatActivity implements View.OnClickListener { private StandardReceiver standardReceiver; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_broad_standard); findViewById(R.id.bt_boardcast).setOnClickListener(this); } @Override public void onClick(View view) { //发送标准广播 Intent intent=new Intent(StandardReceiver.STANDARD_ACTION); sendBroadcast(intent); } @Override protected void onStart() { super.onStart(); standardReceiver = new StandardReceiver(); //意图过滤器 IntentFilter filter=new IntentFilter(StandardReceiver.STANDARD_ACTION); //注册接收器,注册后正常接收 registerReceiver(standardReceiver,filter); } @Override protected void onStop() { super.onStop(); //注册接收器,注销后不再接收广播 unregisterReceiver(standardReceiver); } } package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; public class StandardReceiver extends BroadcastReceiver { public static final String STANDARD_ACTION="com.example.broadcast.standard"; @Override public void onReceive(Context context, Intent intent) { if(intent!=null && intent.getAction().equals(STANDARD_ACTION)){ Log.d("广播:","收到一条广播"); } } } 有序广播 多个接收器接收广播的先后顺序规则： 优先级越大的接收器，越早收到有序广播 优先级相同的时候，越早注册的接收器越早收到有序广播 当然，先接收到的接收者，即可让其它接收器继续接收，也可中断广播不让其它接收者接收 package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import com.example.broadcast.receiver.OrderAReceiver; import com.example.broadcast.receiver.OrderBReceiver; public class BroadOrderActivity extends AppCompatActivity implements View.OnClickListener { public final static String ORDER_ACTION="com.example.broadcast.order"; private OrderAReceiver orderAReceiver; private OrderBReceiver orderBReceiver; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_broad_order); findViewById(R.id.btn_orderboardcast).setOnClickListener(this); } @Override public void onClick(View view) { Intent intent=new Intent(ORDER_ACTION); sendOrderedBroadcast(intent,null); } @Override protected void onStart() { super.onStart(); orderAReceiver = new OrderAReceiver(); IntentFilter Afilter=new IntentFilter(ORDER_ACTION); Afilter.setPriority(11); //设置优先级 registerReceiver(orderAReceiver,Afilter); orderBReceiver = new OrderBReceiver(); IntentFilter Bfilter=new IntentFilter(ORDER_ACTION); Bfilter.setPriority(10); registerReceiver(orderBReceiver,Bfilter); } @Override protected void onStop() { super.onStop(); unregisterReceiver(orderAReceiver); unregisterReceiver(orderBReceiver); } } package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; import com.example.broadcast.BroadOrderActivity; public class OrderAReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if(intent!=null&intent.getAction().equals(BroadOrderActivity.ORDER_ACTION)){ abortBroadcast(); Log.d("信息:","A接收到广播"); } } } package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; import com.example.broadcast.BroadOrderActivity; public class OrderBReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if(intent!=null&intent.getAction().equals(BroadOrderActivity.ORDER_ACTION)){ Log.d("信息:","B接收到广播"); } } } 设置了优先级,A的优先级比B的优先级高,所以A可以中断广播,结果显示B接收不到广播。 广播静态注册 静态注册与代码中直接注册差异不大，，同样需要继承BroadcastReceiver并且写onReceiver方法，但 <receiver android:name=".receiver.ShockReceiver" android:enabled="true" android:exported="true"></receiver> package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Vibrator; import android.util.Log; public class ShockReceiver extends BroadcastReceiver { public static final String SHOCK_ACTION="com.example.broadcat.shock"; @Override public void onReceive(Context context, Intent intent) { if(intent!=null&&intent.getAction().equals(SHOCK_ACTION)){ Log.d("广播","震动"); //获取震动服务 Vibrator vb= (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); //震动多少ms vb.vibrate(500); } } } package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import com.example.broadcast.receiver.ShockReceiver; public class BroadStaticActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_broad_static); findViewById(R.id.btn_send_shock).setOnClickListener(this); } @Override public void onClick(View view) { String fullName="com.example.broadcast.receiver.ShockReceiver"; Intent intent=new Intent(ShockReceiver.SHOCK_ACTION); ComponentName componentName=new ComponentName(this,fullName); intent.setComponent(componentName); sendBroadcast(intent); } } 网络变更广播 主要通过意图过滤器android.net.conn.CONNECTIVITY_CHANGE来对网络变更服务进行广播。 制式类型类： package com.example.broadcast.util; import android.telephony.TelephonyManager; public class NetworkUtil { // 获取数据连接的制式类型 public static String getNetworkClass(int subType) { switch (subType) { case TelephonyManager.NETWORK_TYPE_GPRS: case TelephonyManager.NETWORK_TYPE_EDGE: case TelephonyManager.NETWORK_TYPE_CDMA: case TelephonyManager.NETWORK_TYPE_1xRTT: case TelephonyManager.NETWORK_TYPE_IDEN: return "2G"; case TelephonyManager.NETWORK_TYPE_UMTS: case TelephonyManager.NETWORK_TYPE_EVDO_0: case TelephonyManager.NETWORK_TYPE_EVDO_A: case TelephonyManager.NETWORK_TYPE_HSDPA: case TelephonyManager.NETWORK_TYPE_HSUPA: case TelephonyManager.NETWORK_TYPE_HSPA: case TelephonyManager.NETWORK_TYPE_EVDO_B: case TelephonyManager.NETWORK_TYPE_EHRPD: case TelephonyManager.NETWORK_TYPE_HSPAP: return "3G"; case TelephonyManager.NETWORK_TYPE_LTE: return "4G"; case TelephonyManager.NETWORK_TYPE_NR: return "5G"; default: return "未知"; } } } 接收者类： package com.example.broadcast.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.net.NetworkInfo; import android.util.Log; import com.example.broadcast.util.NetworkUtil; public class SystemnetworkReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if(intent!=null){ NetworkInfo networkInfo=intent.getParcelableExtra("networkInfo"); String text = String.format("收到一个网络变更广播，网络大类为%s，" + "网络小类为%s，网络制式为%s，网络状态为%s", networkInfo.getTypeName(), networkInfo.getSubtypeName(), NetworkUtil.getNetworkClass(networkInfo.getSubtype()), networkInfo.getState().toString()); Log.d("网络变更情况", text); } } } package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import com.example.broadcast.receiver.SystemnetworkReceiver; public class SystemnetworkActivity extends AppCompatActivity { private SystemnetworkReceiver systemnetworkReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_systemnetwork); } @Override protected void onStart() { super.onStart(); systemnetworkReceiver = new SystemnetworkReceiver(); IntentFilter filter = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"); registerReceiver(systemnetworkReceiver, filter); } @Override protected void onStop() { super.onStop(); unregisterReceiver(systemnetworkReceiver); } } 定时管理器 AlarmManager定时管理器： set：设置一次性定时器(安卓6.0之后，在空闲状态下，不保证会发送广播) setAndAllowWhiledle：设置一次性定时器，即使设备处于空闲状态(暗屏等)，也保证执行定时器 setRepeating：设置重复定时器，系统不保证按时发送广播 cancel：取消指定延迟的意图定时器(PendingIntent) 例子： Activity类： package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import com.example.broadcast.receiver.AlarmReceiver; public class AlarmActivity extends AppCompatActivity implements View.OnClickListener { private AlarmReceiver alarmReceiver; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_alarm); findViewById(R.id.btn_send_alarm).setOnClickListener(this); } @Override public void onClick(View view) { Intent intent=new Intent(alarmReceiver.ALARM_ACTION); sendBroadcast(intent); } @Override protected void onStart() { super.onStart(); alarmReceiver = new AlarmReceiver(getApplicationContext()); IntentFilter intentFilter=new IntentFilter(alarmReceiver.ALARM_ACTION); registerReceiver(alarmReceiver,intentFilter); } @Override protected void onStop() { super.onStop(); unregisterReceiver(alarmReceiver); } } 接收者类： package com.example.broadcast.receiver; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Build; import android.util.Log; public class AlarmReceiver extends BroadcastReceiver { private final Context mcontext; public final String ALARM_ACTION="com.example.broadcast.alarm"; public AlarmReceiver(Context applicationContext) { this.mcontext=applicationContext; } @Override public void onReceive(Context context, Intent intent) { if(intent!=null&&intent.getAction().equals(ALARM_ACTION)){ Log.d("信息:","收到一条闹钟广播"); } } public void sendAlarm(){ Intent intent = new Intent(ALARM_ACTION); PendingIntent pendingIntent=PendingIntent.getBroadcast(mcontext,0,intent,PendingIntent.FLAG_IMMUTABLE); //从系统服务中获取闹钟管理器 if(Build.VERSION.SDK_INT>Build.VERSION_CODES.M) { // 允许在空闲时发送广播，Android6.0之后新增的方法 AlarmManager alarmManager = (AlarmManager) mcontext.getSystemService(Context.ALARM_SERVICE); alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, 1000, pendingIntent); } else{ AlarmManager alarmManager = (AlarmManager) mcontext.getSystemService(Context.ALARM_SERVICE); alarmManager.set(AlarmManager.RTC_WAKEUP, 1000, pendingIntent); } } } 横屏竖屏切换 横屏竖屏切换会导致Activity的生命周期被打乱，即会销毁先前的Activity，重新开始一个Activity。 例子： package com.example.broadcast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.res.Configuration; import android.os.Bundle; import android.util.Log; import android.widget.TextView; public class ChangeDirectionActivity extends AppCompatActivity { private TextView tv_monitor; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_change_direction); tv_monitor = findViewById(R.id.tv_text); Log.d("信息:","create启动"); } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); switch (newConfig.orientation){ case Configuration.ORIENTATION_PORTRAIT: tv_monitor.setText("当前屏幕为竖屏方向"); break; case Configuration.ORIENTATION_LANDSCAPE: tv_monitor.setText("当前屏幕为横屏方向"); break; default: break; } } } 在通过自定义配置后，切换横竖屏不会出现Activity重新启动的效果。 画中画 package com.example.broadcast; import androidx.appcompat.app.AppCompatActivity; import android.app.PictureInPictureParams; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.util.Rational; public class ReturnDesktopActivity extends AppCompatActivity { private DesktopReceiver desktopReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_return_desktop); desktopReceiver = new DesktopReceiver(); IntentFilter filter=new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); registerReceiver(desktopReceiver,filter); } @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(desktopReceiver); } //在进入画中画模式和退出画中画模式触发 @Override public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); if(isInPictureInPictureMode){ Log.d("消息","进入画中画模式"); } else{ Log.d("消息","退出画中画模式"); } } private class DesktopReceiver extends BroadcastReceiver{ @Override public void onReceive(Context context, Intent intent) { if(intent!=null&&intent.getAction().equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)){ String reason=intent.getStringExtra("reason"); if(!TextUtils.isEmpty(reason)&&(reason.equals("homekey"))||reason.equals("recentapps")){ //Android8.0后可开启画中画模式 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O &&!isInPictureInPictureMode()){ PictureInPictureParams.Builder builder=new PictureInPictureParams.Builder(); //下面10/5=2,表示画中画窗口宽度是高度两倍 Rational rational=new Rational(10,5); //进入画中画模式 builder.setAspectRatio(rational); enterPictureInPictureMode(builder.build()); } } } } } }
前言 最近在有在持续学习Java的安卓开发，不断的把知识记录下。 工具 Android Studio安装 Studio安装 SDK下载 SDK即软件开发工具包，可将App源码编译成可执行的App应用 创建项目 创建项目 这里Minimum SDK后面指的是兼容到安卓几，表示你的应用支持的最低版本，看百分比选，一般目前的都是9.0-10.0。 内置模拟器 初始化Gradle要下载半小时，内置模拟器也有十多G，默认在C盘很不爽，可以移动， 注册环境变量，然后修改下面的配置，直接把目录移动到想存的位置即可。 观察App运行日志 Android采用Log日志打印日志，各类日志划分： Log.e 表示错误信息，可能导致程序崩溃 Log.w：表示警告信息 Log.i：表示一般消息 Log.d：表示调试信息，可把程序运行变量值打印，方便跟踪调试 Log.v：表示冗余信息 真机调试 1、使用数据库连接到电脑 2、电脑上会自动安装USB存储设备驱动 3、打开手机的开发者选项并开启USB调试 4、将连接的手机设备文件传输模式，并允许计算机进行USB调试 关于安卓开发语言 App开发分为两大技术路线，分别为原生开发和混合开发，官方的编程语言包括Java和Kotlin。 混合开发渲染性能没有原生开发好，但是混合开发可以跨平台，版本更新不需要重新下载API文件。 关于C和C++，Java是解释型语言，在处理图像和音视频时性能显然就有瓶颈，而C和C++是编译型语言，会 先翻译成机器语言，在图像和音视频处理时可以调用java的JNI接口调用C和C++程序进行处理，也称NDK。 目录结构 mainfests：App运行配置文件 java子目录：存放java源代码，后面两个包存放测试用的代码 res目录下： drawable：存放图形描述和图片文件 Layout：App页面的布局文件 mipmap：App的启动图标 values：存放常量的文件，如样式风格等 Gradle Scripts目录下： build.gradle：描述项目级和模块级的App工程编译规则 proguard-rules.pro：java代码的混淆规则，保证安全性 gradle.properties：用于编译工程的命令行参数 local.properties：本地配置文件，描述开发者电脑的环境配置 settings.gradle：配置了需要编译的模块 build.gradle的文件概述解释 plugins { //使用的插件 id 'com.android.application' } android { namespace 'com.example.myapplication' compileSdk 33 //编译使用的SDK版本号,33表示使用Android13 defaultConfig { // App的包名 applicationId "com.example.myapplication" //指定App适合运行的最小的SDK版本号,28表示最小在Android9上运行 minSdk 28 //目标设备的版本号,表示App最希望在那个版本的Android上运行 targetSdk 33 //App的应用版本号 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } //跟单元测试相关 buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } //指定App编译的依赖包信息 dependencies { //指定编译的Android高版本支持库 implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.3' //指定单元测试编译用的junit版本号 testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } 清单文件 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.MyApplication" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> android:icon：App手机屏幕显示的图标 android:label：App手机屏幕显示的名称 android:allowBackup：是否允许数据备份，数据丢失时恢复应用 android:theme：指定App显示的风格 android:supportsRtl：是否支持波斯语等从右向左的文字排列顺序语言 activity应用程序组件，提供屏幕，用来交互完成某项任务 android:exported：当前Activity是否可以被另一个Application的组件启动 android:name：启动应用首先启动那个Applicaiton 简单控件即UI 一、界面显示与逻辑处理 创建新的activity页面 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/text2" /> </LinearLayout> 创建新的MainActivity类，并将类加入Mainifest package com.example.myapplication; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; public class MainActivity2 extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); } } <activity android:name=".MainActivity2"/> 设置跳转按钮 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!"/> </LinearLayout> 写跳转方法 package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv=findViewById(R.id.tv); tv.setText("你好,世界!"); Button button=findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent=new Intent(); intent.setClass(MainActivity.this,MainActivity2.class); startActivity(intent); } }); } } 二、文本 文本大小 px ：图像元素，跟随屏幕大小和像素数量关系变化。 resolution：屏幕垂直和水平方向的像素数量。 dpi：像素密度，指屏幕上每英寸距离中有多少个像素点。 Density：指定每平方英寸含有的像素点数量。 dip/dp：长度单位，同一个单位在不同设备中有不同显示效果。 sp：专门用于设置字体，大小会根据系统的字体大小改变而改变。 dip是开发中的长度单位，最后也要转换为px，px=dip*dpi/160 例：如 320*480分辨率 3.6寸的手机: dpi=(320^2+480^2)^(1/2)/3.6=160 所以 1dp=1px 对于相同尺寸的手机，即使分辨率不同，占用屏幕比例也相同 设置文本颜色 设置文本的颜色一般可直接设置，也可以通过十六进制设置，如0xFFFFEEDD, 其中先 后十六进制顺序为透明度，红色，绿色，蓝色。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:id="@+id/text_color" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30sp" android:text="直接设置文本颜色" /> <TextView android:id="@+id/text_hex_color" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30sp" android:textColor="@color/purple_200" android:text="十六进制设置文本颜色"/> <TextView android:id="@+id/text_code_color" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30sp" android:text="代码中设置文本颜色"/> <TextView android:id="@+id/text_background" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30sp" android:text="设置文本背景颜色" android:background="#ff00ff00"/> </LinearLayout> public class MainTextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main_text); TextView text_color=findViewById(R.id.text_color); text_color.setTextColor(Color.GREEN); TextView text_code_color=findViewById(R.id.text_code_color); text_code_color.setTextColor(0xff00ff00); } } 视图 wrap_content：自适应大小，刚好能包含内容 match_parent：当前控件的大小和父布局的大小一样 dp：自行设置dp大小 在代码中设置视图宽高。 调用控件对象getLayoutParams方法获取控件的布局参数。 调用控件对象的setLayoutParams方法，填入修改后布局参数使之生效。 public class ViewBorderActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_view_border); TextView text_borer=findViewById(R.id.text_view_border); ViewGroup.LayoutParams params=text_borer.getLayoutParams(); params.width= Utils.dip2px(this,300); text_borer.setLayoutParams(params); } } public class Utils { //根据分辨率dp转化为像素px public static int dip2px(Context context, float dpValue){ //获取当前像素密度,1个dp对应多少个像素 float scale=context.getResources().getDisplayMetrics().density; return (int)(dpValue*scale+0.5f); } } 三、布局 线性布局LinearLayout orientation为horizontal，内部视图水平方向排列 oritentation为vertical，内部视图竖着排列 相对布局RelativeLayout 相对布局的下级视图位置由其他视图决定，确定位置参照物分两种： （1）与该视图自身评级的视图 （2）该视图的上级视图 不设定，默认在左上角 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/center" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:background="#ffffff" android:text="我在中间" android:textColor="#000000" android:textSize="10sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:background="#ffffff" android:text="我在上级左边中间" android:textColor="#000000" android:textSize="10sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ffffff" android:layout_alignParentBottom="true" android:text="我在上级底部对齐" android:textSize="10sp" android:textColor="#000000"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toLeftOf="@id/center" android:layout_alignTop="@id/center" android:background="#ffffff" android:text="我在中间左边" android:textColor="#000000" android:textSize="10sp" /> </RelativeLayout> 网格布局GridLayout，支持多行多列的表格排列 网格布局默认从左往右，从上到下，属性如下 columnCount，指定网格列数，即每行能放多少视图 rowCount，指定网格行数，每列能放多少个视图 <?xml version="1.0" encoding="utf-8"?> <GridLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:columnCount="2" android:rowCount="2"> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:layout_height="60dp" android:text="红色" android:background="#ff0000" android:textSize="20sp" android:gravity="center" android:textColor="#000000"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:layout_height="60dp" android:text="橙色" android:background="#ffaa00" android:textSize="20sp" android:gravity="center" android:textColor="#000000"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:layout_height="60dp" android:text="绿色" android:background="#00ff00" android:textSize="20sp" android:gravity="center" android:textColor="#000000"/> <TextView android:layout_width="0dp" android:layout_columnWeight="1" android:layout_height="60dp" android:text="紫色" android:background="#660066" android:textSize="20sp" android:gravity="center" android:textColor="#000000"/> </GridLayout> 滚动视图ScrollView ScrollView，垂直方向滚动，垂直方向滚动时，layout_width属性值设置为math_parent，layout_height属性设置为wrap_content HorizontalScrollView，水平方向滚动，layout_width属性值设置为wrap_content，layout_height属性设置为match_parent <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <HorizontalScrollView android:layout_width="wrap_content" android:layout_height="200dp"> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal"> <View android:layout_width="300dp" android:layout_height="match_parent" android:background="#00ff00"> </View> <View android:layout_width="300dp" android:layout_height="match_parent" android:background="#00aaff"> </View> </LinearLayout> </HorizontalScrollView> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="vertical"> <View android:layout_width="match_parent" android:layout_height="400dp" android:background="#ff0000"> </View> <View android:layout_width="match_parent" android:layout_height="400dp" android:background="#aa00ff"> </View> </LinearLayout> </ScrollView> </LinearLayout> 四、按钮 Button由TextView派生，区别在于： Button拥有默认按钮背景(紫色)，而TextView无背景。 Button的内部文本默认居中对齐。 Button会默认将英文字母转为大写，testAllCaps可以设置是否转换。 ImageButton显示图片的图像按钮，继承自ImageView ImageButton只能显示图片，不能显示文本 ImageButton的图像可按比例缩放,Button背景设置的图像会拉伸变形 Button只能靠背景显示一张图片，ImageButton可在前景和背景显示图片，从而实现两张图片叠加效果 例子：设置三个按钮，分别启动和禁用第三个按钮，禁用按钮需要长按才能禁用，第三个按钮点击则显示时间。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> </LinearLayout> <TextView android:id="@+id/button_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="查看测试禁用按钮的结果" android:textSize="17sp" android:textColor="#000000"/> </LinearLayout> package com.example.myapplication.utils; import java.text.SimpleDateFormat; import java.util.Date; public class DateTime { public static String getNowTime(){ SimpleDateFormat simpleDateFormat=new SimpleDateFormat("HH:mm:ss"); return simpleDateFormat.format(new Date()); } } package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.example.myapplication.utils.DateTime; public class ButtonTextActivity extends AppCompatActivity { private Button button_test_click; private TextView button_view; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_button_text); Button button_begin_click=findViewById(R.id.button_begin_click); Button button_disable_click= findViewById(R.id.button_disable_click); button_test_click = findViewById(R.id.button_test_click); button_view = findViewById(R.id.button_text_view); button_begin_click.setOnClickListener(new MyOnclickListener()); button_test_click.setOnClickListener(new MyOnclickListener()); button_disable_click.setOnLongClickListener(new MyOnclickLongListener()); } class MyOnclickListener implements View.OnClickListener{ @Override public void onClick(View view) { switch (view.getId()){ case R.id.button_begin_click: button_test_click.setEnabled(true); break; case R.id.button_test_click: String time=String.format("当前时间:%s", DateTime.getNowTime()); button_view.setText(time); } } } class MyOnclickLongListener implements View.OnLongClickListener{ @Override public boolean onLongClick(View view) { button_test_click.setEnabled(false); return true; } } } 五、控件综合训练(简易计算器) Strings.xml <resources> <string name="app_name">My Application</string> <string name="text2">Activity Main</string> <string name="simple_calculator">简易计算器</string> <string name="cancel">CE</string> <string name="divide">÷</string> <string name="clear">C</string> <string name="multiply">+</string> <string name="ride">*</string> <string name="minus">-</string> <string name="nine">9</string> <string name="eight">8</string> <string name="seven">7</string> <string name="six">6</string> <string name="five">5</string> <string name="four">4</string> <string name="three">3</string> <string name="two">2</string> <string name="one">1</string> <string name="zero">0</string> <string name="reciprocal">1/X</string> <string name="doc">.</string> <string name="equal">=</string> <string name="root">√</string> </resources> dimens.xml <?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="button_font_size">30sp</dimen> <dimen name="button_height">90dp</dimen> </resources> layout.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#EEEEEE" android:orientation="vertical" android:padding="5dp"> <ScrollView android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/simple_calculator" android:textColor="@color/black" android:gravity="center" android:textSize="30sp"/> <TextView android:id="@+id/tv_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" android:text="0" android:textColor="@color/black" android:lines="8" android:textSize="25sp" android:gravity="right|bottom"/> <GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:columnCount="4" android:rowCount="5"> </GridLayout> </LinearLayout> </ScrollView> </LinearLayout> 主类 package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class CalculatorActivity extends AppCompatActivity implements View.OnClickListener { private TextView tv_result; //第一个操作数 private String firstNum=""; //操作符号 private String operator=""; //第二个操作数 private String secodeNum=""; //计算结果 private String result=""; //显示的文本 private String showText=""; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_calculator); tv_result = findViewById(R.id.tv_result); findViewById(R.id.btn_cancel).setOnClickListener(this); findViewById(R.id.btn_divide).setOnClickListener(this); findViewById(R.id.btn_ride).setOnClickListener(this); findViewById(R.id.btn_clear).setOnClickListener(this); findViewById(R.id.btn_seven).setOnClickListener(this); findViewById(R.id.btn_eight).setOnClickListener(this); findViewById(R.id.btn_nine).setOnClickListener(this); findViewById(R.id.btn_multi).setOnClickListener(this); findViewById(R.id.btn_four).setOnClickListener(this); findViewById(R.id.btn_five).setOnClickListener(this); findViewById(R.id.btn_six).setOnClickListener(this); findViewById(R.id.btn_minus).setOnClickListener(this); findViewById(R.id.btn_one).setOnClickListener(this); findViewById(R.id.btn_two).setOnClickListener(this); findViewById(R.id.btn_three).setOnClickListener(this); findViewById(R.id.btn_root).setOnClickListener(this); findViewById(R.id.btn_reciprocal).setOnClickListener(this); findViewById(R.id.btn_zero).setOnClickListener(this); findViewById(R.id.btn_doc).setOnClickListener(this); findViewById(R.id.btn_equal).setOnClickListener(this); } @Override public void onClick(View view) { String inputText; inputText=((TextView) view).getText().toString(); switch (view.getId()){ case R.id.btn_cancel: if(operator.equals("")) { showText = showText.substring(0, showText.length() - 1); refreshOperate(showText); refreshText(showText); break; } if(secodeNum.equals("")&&!operator.equals("")){ showText = showText.substring(0, showText.length() - 1); CE(); refreshText(showText); break; } else{ showText = showText.substring(0, showText.length() - 1); refreshOperate(showText); refreshText(result); break; } case R.id.btn_clear: clear(); break; case R.id.btn_multi: case R.id.btn_divide: case R.id.btn_ride: case R.id.btn_minus: operator=inputText; refreshText(showText+operator); break; case R.id.btn_equal: double calculate_result=calculateFour(); refreshOperate(String.valueOf(calculate_result)); refreshText(showText+"="+result); break; //开根号 case R.id.btn_root: double sqrt_result=Math.sqrt(Double.parseDouble(firstNum)); refreshOperate(String.valueOf(sqrt_result)); refreshText(showText+"√="+result); break; //求导数 case R.id.btn_reciprocal: double reciprocal_result=1.0/Double.parseDouble(firstNum); refreshOperate(String.valueOf(reciprocal_result)); refreshText(showText+"/="+result); break; default: if (result.length()>0&&operator.equals("")){ clear(); } if(operator.equals("")){ firstNum=firstNum+inputText; } else{ secodeNum=secodeNum+inputText; } if(showText.equals("0")&&!inputText.equals(".")){ refreshText(inputText); } else { refreshText(showText + inputText); } break; } } private void cancel(){ } private void clear(){ refreshOperate(""); refreshText(""); } private double calculateFour(){ switch (operator){ case "+": return Double.parseDouble(firstNum)+Double.parseDouble(secodeNum); case "-": return Double.parseDouble(firstNum)-Double.parseDouble(secodeNum); case "*": return Double.parseDouble(firstNum)*Double.parseDouble(secodeNum); default: return Double.parseDouble(firstNum)/Double.parseDouble(secodeNum); } } private void CE(){ //7+ operator=""; } private void refreshOperate(String new_result){ result=new_result; firstNum=result; secodeNum=""; operator=""; } private void refreshText(String text){ showText=text; tv_result.setText(showText); } } 并非是什么完美计算器，仅是复习以上知识操作使用。 六、Activity 从当前页面跳转到新页面： startActivity(new intent(源页面.this,目标页面.class)) 当前页面返回上一个页面： finish(); activity的生命周期： onCreate 创建活动，把页面加载进内存，进入初始状态 onStart 开始活动，把活动页面显示到屏幕，就绪态 onResume 恢复活动，活动页面进入活跃状态 onPause 暂停活动，页面不能正常交互 onStop 停止活动，页面不在屏幕 onDestroy 销毁活动，回收系统资源，从内存清除 onRestart 重启活动，重新加载内存的页面数据 onNewIntent 重用已有的实例 Activity启动模式(栈) 默认启动模式standard，启动的Activity依照顺序被依次压入Task栈。 栈顶复用模式singleTop，栈顶Activity为我们新建的Activity，不会重新创建新的Activity。 栈内复用模式singleTask，栈内存在目标Activity实例，则将task内对应Activity实例之上的Activity出栈，并 将对应Activity置于栈顶。 全局唯一模式singleInstance，为目标Activity创建新的Task栈，新的Task栈只有这一个Activity，如 已经创建目标Activity，则不创建新的Task，将以前的Activity唤醒。 启动模式的设置：intent.setFlags()代码中启动，Mainfest.xml中动态设置launch。 例子：登录页面进行登录成功后，即使后退，也不能返回登录页面，而是直接退出程序。 package com.example.activity; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; public class LoginInputActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login_input); Button btn_login=findViewById(R.id.btn_login); btn_login.setOnClickListener(new MyOnclickListen()); } class MyOnclickListen implements View.OnClickListener{ @Override public void onClick(View view) { Intent intent=new Intent(LoginInputActivity.this,LoginSuccessActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } } } 通过设置启动模式来完成，一旦登录则会清空当前活动栈中的所有实例，然后再同时启动开辟新任务的活动栈，这样登录后，再点击返回则会直接退出程序。 Intent Intent各个组件之间信息沟通桥梁，用于组件之间通信，主要完成工作是本次通信请求从哪来，到哪去，要怎么走 发起方携带本次通信需要的数据，接收方从收到的意图中解析数据。 发起方若判断接收方处理结果，意图就要负责让接收方回应答数据。 显示Intent，直接指定来源活动与目标活动，精确匹配，三种方式 直接Intent函数中指定：new Intent() 调用意图对象：setClass() 调用意图对象setComponent() 隐式Intent，没有明确指定要跳转的目标活动，只给出一个动作字符串匹配，属于模糊匹配 通过setAction，url，setData()指定动作和数据 常见的隐式动作有： 例子1：使用隐式Intent直接进行拨号，发送短信，或跳转到其它APP package com.example.activity; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.view.View; import android.widget.Button; public class IntentActivity extends AppCompatActivity implements View.OnClickListener{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_intent); Button btn_dail=findViewById(R.id.btn_dail); Button btn_cms=findViewById(R.id.btn_cms); Button btn_my=findViewById(R.id.btn_mypage); btn_dail.setOnClickListener(this); btn_cms.setOnClickListener(this); btn_my.setOnClickListener(this); } @Override public void onClick(View view) { String phone="123456"; Intent intent=new Intent(); switch (view.getId()){ case R.id.btn_dail: intent.setAction(Intent.ACTION_DIAL); Uri uri=Uri.parse("tel:"+phone); intent.setData(uri); startActivity(intent); break; case R.id.btn_cms: intent.setAction(Intent.ACTION_SENDTO); Uri uri2=Uri.parse("smsto:"+phone); intent.setData(uri2); startActivity(intent); break; case R.id.btn_mypage: intent.setAction("android.intent.action.Aiwin"); intent.addCategory(Intent.CATEGORY_DEFAULT); startActivity(intent); break; } } } <intent-filter> <action android:name="android.intent.action.Aiwin" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> 最后跳转自己的APP，需要在mainfestxml中添加intent-filter 例子2：A页面发送信息给B页面，B页面再返回回应信息给A页面，需要通过registerForActivityResult() package com.example.activity; import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContract; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class ActRequestActivity extends AppCompatActivity implements View.OnClickListener{ private ActivityResultLauncher<Intent> register; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_act_request); Button btn_send=findViewById(R.id.btn_request); TextView btn_receive=findViewById(R.id.btn_receive); btn_send.setOnClickListener(this); register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { if(result!=null){ Intent intent=result.getData(); if(intent!=null&result.getResultCode()== Activity.RESULT_OK){ Bundle bundle=intent.getExtras(); String message=bundle.getString("response"); btn_receive.setText(message); } } } }); } @Override public void onClick(View view) { Intent intent=new Intent(this,ActResponseActivity.class); Bundle bundle=new Bundle(); bundle.putString("message","你睡了吗?"); intent.putExtras(bundle); register.launch(intent); } } package com.example.activity; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class ActResponseActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_act_response); TextView request=findViewById(R.id.btn_request); Button btn_response=findViewById(R.id.btn_response); Bundle bundle=getIntent().getExtras(); String request_content=bundle.getString("message"); request.setText(request_content); btn_response.setOnClickListener(this); } @Override public void onClick(View view) { Intent intent=new Intent(); Bundle bundle=new Bundle(); bundle.putString("response","我没睡,我爸妈不在家"); intent.putExtras(bundle); setResult(ActRequestActivity.RESULT_OK,intent); finish(); } } 应用页面注册快捷方式 在安卓7以后，可以长按页面图标，弹出一些快捷方式，比如长按微信会弹出扫一扫，付款码等 点击即可直接跳转到该页面。 例子：在xml目录创建shortcuts.xml，先修改Mainfest.xml文件 <activity android:name=".ActStartActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" /> </activity> 配置shortcuts.xml文件 <shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> <shortcut android:enabled="true" android:icon="@mipmap/ic_launcher" android:shortcutId="first" android:shortcutLongLabel="@string/shortcutLongLabel_first" android:shortcutShortLabel="@string/shortcutShortLabel_first"> <intent android:action="android.intent.action.VIEW" android:targetClass="com.example.activity.ActStartActivity" android:targetPackage="com.example.activity" /> <categories android:name="android.shortcut.conversation" /> </shortcut> <shortcut android:enabled="true" android:icon="@mipmap/ic_launcher" android:shortcutId="second" android:shortcutLongLabel="@string/shortcutLongLabel_second" android:shortcutShortLabel="@string/shortcutShortLabel_second"> <intent android:action="android.intent.action.VIEW" android:targetClass="com.example.activity.ActStartActivity" android:targetPackage="com.example.activity" /> <categories android:name="android.shortcut.conversation" /> </shortcut> </shortcuts> 效果： 七. 中级控件 Shape图 形状图形 Shape，形状图形的定义文件是以shape标签为根节点的XML描述文件，一般有四种类型的形状： rectangle 矩形，默认值 oval 椭圆，corners节点失效 line 直线，必须设置stroke节点 ring 圆环 其中XML中能设置的有：size尺寸，stroker描边，corners圆角，solid填充，padding间隔，gradient渐变 如设置一个椭圆形： <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#ff66aa"/> <stroke android:width="1dp" android:color="#aaaaaa"/> </shape> 九宫格图片 在Android Stdio中，可以直接把PNG图片转成九宫格图片，利用九宫格图片，能够使图片被拉伸但是不变形， 能够用于适应不同的手机屏幕。 状态列表图形 简单来说，就是按钮被按下和没被按下显示不同的颜色或者形状等，以便于用户区分按了哪一个按钮，可通过StateListDrawable完成。状态的取值常用的有pressed、checked、focused、selected <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@android:drawable/button_onoff_indicator_on"/> <item android:drawable="@android:drawable/button_onoff_indicator_off"/> </selector> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> </LinearLayout> checkbox <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <CheckBox android:id="@+id/ck_system" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="5dp" android:text="系统的CheckBox" /> <CheckBox android:id="@+id/ck_custom" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:button="@drawable/checkbox_selector" android:padding="5dp" android:text="CheckBox换图标"/> </LinearLayout> package com.example.middlecontrolactivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.CheckBox; import android.widget.CompoundButton; public class CheckBoxAcivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_check_box_acivity); CheckBox ck_system=findViewById(R.id.ck_system); CheckBox ck_custom=findViewById(R.id.ck_custom); ck_system.setOnCheckedChangeListener(this); ck_custom.setOnCheckedChangeListener(this); } @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { String desc=String.format("您%s了这个CheckBox",b ? "勾选":"没勾选"); compoundButton.setText(desc); } } Switch 与checkbox使用是一致的，都是从Compuound类继承而来 textOn:设置右侧开启时文本 textOff:设置左侧关闭时文本 track:设置开关轨道的背景 thumb:设置开关标识图标 RadioButton 一般单选按钮只允许选择一个，需要放在RadioGroup里面，对RadioGroup进行监听 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="请选择你的性别" android:textColor="@color/black" android:textSize="17sp"/> <RadioGroup android:id="@+id/rg_rb" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <RadioButton android:id="@+id/rb_male" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:text="男"/> <RadioButton android:id="@+id/rb_female" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="女"/> </RadioGroup> <TextView android:id="@+id/tv_result" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> package com.example.middlecontrolactivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.CompoundButton; import android.widget.RadioGroup; import android.widget.TextView; public class RadioButtonActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener { private TextView tv_result; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_radio_button); RadioGroup rg_rb=findViewById(R.id.rg_rb); tv_result=findViewById(R.id.tv_result); rg_rb.setOnCheckedChangeListener(this); } @Override public void onCheckedChanged(RadioGroup radioGroup, int i) { switch (i){ case R.id.rb_male: tv_result.setText("你选择的是男性"); break; case R.id.rb_female: tv_result.setText("你选择的女性"); break; } } } EditText、焦点变更事件、文本变化监听器 焦点变更事件可以对鼠标的焦点变更进行监听，比如登录要输入手机号+密码，可以对密码进行焦点监听，当输入密码时，检测手机号是否为11位，若不为，则不能输入密码。 文本变化监听器则可通过对EditText进行监听做出相应的动作，比如当手机号输够十一位后自动关闭键盘。 hint:指定提示文本的内容 maxLength:指定文件允许输入的最大长度 inputType:指定输入的文本类型 输入类型 说明 text 文本 testPassword 文本密码 number 整型数 numberSigned 带符号的数字 numberDecimal 带小数点的数字 numberPassword 数字密码 datetime 时间日期格式，数字，横线，斜杆，空格，冒号 date 日期格式，数字，横线，斜杠 time 时间格式 例子：简单的登录判断程序 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="5dp" android:orientation="vertical"> <EditText android:id="@+id/phone" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入11位手机号" android:maxLength="11" android:inputType="phone" android:textColorHint="@color/black" /> <EditText android:id="@+id/password" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入密码" android:inputType="textPassword" android:textColorHint="@color/black"/> <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="17sp"/> </LinearLayout> package com.example.middlecontrolactivity; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.example.middlecontrolactivity.util.ViewUtil; public class EditTextActivity extends AppCompatActivity implements View.OnClickListener, View.OnFocusChangeListener { private EditText phone; private EditText password; private TextView v; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit_text); v=findViewById(R.id.text); Button button = findViewById(R.id.button); phone=findViewById(R.id.phone); password=findViewById(R.id.password); button.setOnClickListener(this); password.setOnFocusChangeListener(this);//焦点变更事件 phone.addTextChangedListener(new HideTextWatcher(phone,11)); //输够位数则自动关闭键盘(文本监听) password.addTextChangedListener(new HideTextWatcher(phone,6));//输够位数则自动关闭键盘 } @Override public void onClick(View view) { String p=phone.getText().toString(); String pass=password.getText().toString(); if(p.equals("123456") && pass.equals("123456")){ v.setText("登录成功"); } else{ v.setText("登录失败"); } } @Override public void onFocusChange(View view, boolean b) { if(b){ String sure_phone=phone.getText().toString(); if(TextUtils.isEmpty(sure_phone)||sure_phone.length()<11){ phone.requestFocus(); Toast.makeText(this,"请输入11位号码",Toast.LENGTH_SHORT).show(); } } } private class HideTextWatcher implements TextWatcher{ private EditText mView; private int mMaxLength; public HideTextWatcher(EditText v, int maxLength) { mView=v; mMaxLength=maxLength; } @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void afterTextChanged(Editable editable) { String str=editable.toString(); if(str.length()==mMaxLength){ ViewUtil.hideOneInputMethod(EditTextActivity.this,mView); } } } } package com.example.middlecontrolactivity.util; import android.app.Activity; import android.content.Context; import android.view.View; import android.view.inputmethod.InputMethodManager; public class ViewUtil { public static void hideOneInputMethod(Activity act, View v){ //从系统服务中获取到输入法管理器 InputMethodManager imm= (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE); //通过token关闭屏幕上的输入法键盘 imm.hideSoftInputFromWindow(v.getWindowToken(),0); } } 提醒对话框 就是类似于卸载手机应用时的弹出的框，选择确定或者取消，分别对不同的选择进行不同的监听。 package com.example.middlecontrolactivity; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class AlertDialogActivity extends AppCompatActivity implements View.OnClickListener { private Button button; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_alert_dialog); button=findViewById(R.id.alert_btn); textView=findViewById(R.id.text); button.setOnClickListener(this); } @Override public void onClick(View view) { AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setTitle("亲爱的用户"); builder.setMessage("你真的要卸载吗?"); builder.setPositiveButton("确定",(dialog,which)->{ textView.setText("虽然依依不舍,但是终究离开"); }); builder.setNegativeButton("再想想",(dialog,which)->{ textView.setText("我依旧会陪伴"); }); AlertDialog alertDialog=builder.create(); alertDialog.show(); } } 日期、时间对话框 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <DatePicker android:id="@+id/dp_date" android:layout_width="match_parent" android:layout_height="wrap_content" android:datePickerMode="spinner" android:calendarViewShown="false"/> <TextView android:id="@+id/date_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="17sp"/> </LinearLayout> package com.example.middlecontrolactivity; import androidx.appcompat.app.AppCompatActivity; import android.app.Dialog; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.DatePicker; import android.widget.TextView; import org.w3c.dom.Text; import java.util.Calendar; public class DatePickerDialog extends AppCompatActivity implements View.OnClickListener, android.app.DatePickerDialog.OnDateSetListener { private Button btn_date; private Button btn_sure; private DatePicker dp_date; private TextView date_text; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_date_picker_dialog); btn_date=findViewById(R.id.btn_date); btn_sure=findViewById(R.id.btn_sure); dp_date=findViewById(R.id.dp_date); date_text=findViewById(R.id.date_text); btn_date.setOnClickListener(this); btn_sure.setOnClickListener(this); } @Override public void onClick(View view) { switch (view.getId()){ case R.id.btn_sure: String desc=String.format("你选择的日期是%s年%s月%s日",dp_date.getYear(),dp_date.getMonth()+1,dp_date.getDayOfMonth()); date_text.setText(desc); break; case R.id.btn_date: Calendar calendar=Calendar.getInstance(); int year= calendar.get(Calendar.YEAR); int month= calendar.get(Calendar.MONTH); int day= calendar.get(Calendar.DAY_OF_MONTH); android.app.DatePickerDialog dialog=new android.app.DatePickerDialog(this,this,year,month,day); dialog.show(); break; } } @Override public void onDateSet(DatePicker datePicker, int year, int month, int day) { String desc=String.format("你选择的日期是%s年%s月%s日",year,month+1,day); date_text.setText(desc); } } 日期和时间所用的方式是一致的，只需将DatePicker改为TimePicker即可，同时DatePickerDialog改为TimePickerDialog即可。 八、综合案列(登录与找回密码) 设置两个页面，通过两种方式登录，手机号与验证码登录，并且可以跳转到另外的页面找回密码，通过Intent包裹数据进行不同页面的传输和传回， 并对手机号和密码，验证码框进行监听，达到长度则撤销键盘，达不到长度或不正确弹出提醒对话框。 UI界面涉及： login.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RadioGroup android:id="@+id/rg_login" android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <RadioButton android:id="@+id/rb_password" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/login_by_password" android:checked="true"/> <RadioButton android:id="@+id/rb_verifycode" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/login_by_verifycode"/> </RadioGroup> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/phone_number" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <EditText android:id="@+id/et_phone" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:hint="@string/input_phone_number" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="11" android:textColor="@color/black" android:inputType="phone" android:textSize="@dimen/common_font_size"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:id="@+id/tv_password" android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/login_password" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <RelativeLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1"> <EditText android:id="@+id/et_password" android:layout_width="match_parent" android:layout_height="match_parent" android:hint="@string/input_password" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="6" android:textColor="@color/black" android:inputType="numberPassword" android:textSize="@dimen/common_font_size"/> </RelativeLayout> </LinearLayout> <CheckBox android:id="@+id/ck_remember" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/remember_password" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> </LinearLayout> forgetpassword.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/input_new_pasword" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <EditText android:id="@+id/et_password" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:hint="@string/input_new_password" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="11" android:textColor="@color/black" android:inputType="phone" android:textSize="@dimen/common_font_size"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:id="@+id/new_password" android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/sure_new_password" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <EditText android:id="@+id/sure_new_password" android:layout_width="match_parent" android:layout_height="match_parent" android:hint="@string/input_new_password_again" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="6" android:textColor="@color/black" android:inputType="numberPassword" android:textSize="@dimen/common_font_size"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="@dimen/item_layout_height" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/verifycode" android:gravity="center" android:textColor="@color/black" android:textSize="@dimen/common_font_size"/> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:id="@+id/et_verifycode" android:layout_width="match_parent" android:layout_height="match_parent" android:hint="@string/input_verifycode" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:maxLength="6" android:textColor="@color/black" android:inputType="numberPassword" android:textSize="@dimen/common_font_size"/> </RelativeLayout> </LinearLayout> </LinearLayout> strings.xml: <resources> <string name="app_name">MiddleControlActivity</string> <string name="login_by_password">密码登录</string> <string name="login_by_verifycode">验证码登录</string> <string name="phone_number">手机号码:</string> <string name="input_phone_number">请输入手机号码</string> <string name="login_password">登录密码:</string> <string name="input_password">请输入密码</string> <string name="forget_password">忘记密码</string> <string name="remember_password">记住密码</string> <string name="login">登 录</string> <string name="input_new_pasword">输入新密码</string> <string name="input_new_password">请输入新密码</string> <string name="sure_new_password">确认新密码</string> <string name="input_new_password_again">请再次输入新密码</string> <string name="verifycode"> 验证码:</string> <string name="input_verifycode">请输入验证码</string> <string name="get_verifycode">获取验证码</string> <string name="done">确 定</string> </resources> dimens.xml: <?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="common_font_size">17sp</dimen> <dimen name="button_font_size">20sp</dimen> <dimen name="item_layout_height">50dp</dimen> </resources> 登录逻辑： package com.example.middlecontrolactivity; import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; import android.widget.Toast; import com.example.middlecontrolactivity.util.ViewUtil; import java.util.Random; public class LoginMainActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener, View.OnClickListener { private TextView tv_password; private EditText et_password; private Button btn_forget; private CheckBox ck_remember; private EditText et_phone; private RadioButton rb_password; private RadioButton rb_verifycode; private ActivityResultLauncher<Intent> register; private Button btn_login; private String mPassword="123456"; private String mVerifyCode; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login_main); RadioGroup rb_login=findViewById(R.id.rg_login); et_phone = findViewById(R.id.et_phone); tv_password=findViewById(R.id.tv_password); et_password=findViewById(R.id.et_password); btn_forget=findViewById(R.id.btn_forget); ck_remember=findViewById(R.id.ck_remember); rb_password = findViewById(R.id.rb_password); rb_verifycode=findViewById(R.id.rb_verifycode); btn_login=findViewById(R.id.btn_login); btn_login.setOnClickListener(this); et_phone.addTextChangedListener(new HideTextWatcher(et_phone,11)); et_password.addTextChangedListener(new HideTextWatcher(et_password,6)); rb_login.setOnCheckedChangeListener(this); btn_forget.setOnClickListener(this); register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { Intent intent=result.getData(); if(intent!=null&&result.getResultCode()== Activity.RESULT_OK) { mPassword=intent.getStringExtra("new_password"); } } }); } @SuppressLint("NonConstantResourceId") @Override public void onCheckedChanged(RadioGroup radioGroup, int checkId) { switch (checkId){ case R.id.rb_password: tv_password.setText(R.string.login_password); et_password.setHint(R.string.input_password); btn_forget.setText(R.string.forget_password); ck_remember.setVisibility(View.VISIBLE); break; case R.id.rb_verifycode: tv_password.setText(R.string.verifycode); et_password.setHint(R.string.input_verifycode); btn_forget.setText(R.string.get_verifycode); ck_remember.setVisibility(View.GONE); break; } } @SuppressLint({"DefaultLocale", "NonConstantResourceId"}) @Override public void onClick(View view) { String phone=et_phone.getText().toString(); switch (view.getId()){ case R.id.btn_forget: if(phone.length()<11){ Toast.makeText(this,"请输入正确的手机号",Toast.LENGTH_SHORT).show(); return ; } if(rb_password.isChecked()){ //通过Intent包裹数据传递到忘记密码界面 Intent intent=new Intent(this,ForgetPasswordActivity.class); intent.putExtra("phone",phone); register.launch(intent); } else if(rb_verifycode.isChecked()){ //弹出对话框,提醒用户记住 mVerifyCode=String.format("%06d",new Random().nextInt(999999)); AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setTitle("请记住验证码"); builder.setMessage("手机号"+phone+",本次验证码是"+mVerifyCode+",请输入验证码"); builder.setPositiveButton("好的",null); AlertDialog dialog=builder.create(); dialog.show(); } break; case R.id.btn_login: if(rb_password.isChecked()){ if(!mPassword.equals(et_password.getText().toString())){ Toast.makeText(this,"请输入正确的密码",Toast.LENGTH_SHORT) .show(); return ; } loginSuccess(); } else if(rb_verifycode.isChecked()){ if(!mVerifyCode.equals(et_password.getText().toString())){ Toast.makeText(this,"请输入正确的验证码",Toast.LENGTH_SHORT); return; } loginSuccess() ; } break; } } //EditText监听器,到达位数则自动关闭键盘 private class HideTextWatcher implements TextWatcher { private EditText mView; private int mLength; public HideTextWatcher(EditText e, int MaxLength) { this.mView=e; this.mLength=MaxLength ; } @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void afterTextChanged(Editable editable) { if(editable.length()==mLength){ ViewUtil.hideOneInputMethod(LoginMainActivity.this,mView); } } } private void loginSuccess(){ String desc=String.format("您的手机号码是%s,恭喜你通过登录验证,点击确定返回",et_phone.getText().toString()); AlertDialog.Builder builder=new AlertDialog.Builder(this) ; builder.setMessage(desc); builder.setPositiveButton("确定返回", (dialogInterface, i) -> { finish(); }); builder.setNegativeButton("我再看看",null); Dialog dialog=builder.create(); dialog.show(); } } 找回密码逻辑： package com.example.middlecontrolactivity; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.Toast; import java.util.Random; public class ForgetPasswordActivity extends AppCompatActivity implements View.OnClickListener { private String mPhone; private String mVerifyCode; private EditText et_password; private EditText sure_password; private EditText et_verifycode; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_forget_password); mPhone=getIntent().getStringExtra("phone"); et_password=findViewById(R.id.et_password); sure_password=findViewById(R.id.sure_new_password); findViewById(R.id.get_verifycode).setOnClickListener(this); findViewById(R.id.confirm).setOnClickListener(this); et_verifycode=findViewById(R.id.et_verifycode); } @SuppressLint({"NonConstantResourceId", "DefaultLocale"}) @Override public void onClick(View view) { switch (view.getId()){ case R.id.get_verifycode: mVerifyCode=String.format("%06d",new Random().nextInt(999999)); AlertDialog.Builder builder=new AlertDialog.Builder(this); builder.setTitle("请记住验证码"); builder.setMessage("手机号"+mPhone+",本次验证码是"+mVerifyCode+",请输入验证码"); builder.setPositiveButton("好的",null); AlertDialog dialog=builder.create(); dialog.show(); break; case R.id.confirm: String password=et_password.getText().toString(); String password_second=sure_password.getText().toString(); String verifyCode=et_verifycode.getText().toString(); if(password.length()<6){ Toast.makeText(this,"请输入6位的密码",Toast.LENGTH_SHORT).show(); return; } if(!password.equals(password_second)){ Toast.makeText(this,"两次密码不一致",Toast.LENGTH_SHORT).show(); return; } if(!verifyCode.equals(mVerifyCode)){ Toast.makeText(this,"验证码不正确",Toast.LENGTH_SHORT).show(); return ; } Toast.makeText(this,"密码修改成功",Toast.LENGTH_SHORT).show(); Intent intent=new Intent(); intent.putExtra("new_password",password); setResult(Activity.RESULT_OK,intent); finish(); break; } } } 数据存储 一、SharedPreferences SharePreferences是Android的轻量级存储工具，采用存储结构是Key-Value的键值对方式，重量级存储的是SQLite数据库 共享参数的存储介质是符合XML规范的配置文件，保存路径是：/data/data/应用包名/shared_prefs/文件名.xml。 使用场景： 简单且孤立的数据，文本形式的数据，需持久化存储的数据(在App退出后再次启动，保存数据依旧有效)，实际开发中，经常存储用户的个性化配置信息，行为信息，临时需要保存的片段信息等。 例子：一个页面用于提交个人的姓名，年龄，身高，体重，是否结婚的信息，提交后，关闭程序再次打开信息依旧能够显示。 可以使用sharedPreferences用于存储提交过的信息，存储在xml中，再应用程序再次打开时，通过getSharedPreferences从中获取，并设置edittext的内容即可。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="姓名:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的姓名" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="年龄:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/age" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的年龄" android:textSize="17sp" android:inputType="number" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="身高:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/height" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的身高" android:textSize="17sp" android:inputType="number" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="体重:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/weight" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入您的体重" android:inputType="number" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> <CheckBox android:id="@+id/married" android:layout_width="match_parent" android:layout_height="wrap_content" android:checked="false" android:textColor="@color/black" android:text="已婚" android:textSize="17sp" /> </LinearLayout> package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.content.SharedPreferences; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.Toast; public class ShareWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private Button save; private SharedPreferences sharedPreferences; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_share_write); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); save=findViewById(R.id.save); sharedPreferences = getSharedPreferences("personalInformation",MODE_PRIVATE); save.setOnClickListener(this); reload(); } public void reload(){ String re_name=sharedPreferences.getString("name",null); String re_age= String.valueOf(sharedPreferences.getInt("age",0)); String re_weight= String.valueOf(sharedPreferences.getFloat("weight",0f)); String re_height= String.valueOf(sharedPreferences.getFloat("height",0f)); boolean re_married=sharedPreferences.getBoolean("married",false); name.setText(re_name); age.setText(re_age); weight.setText(re_weight); height.setText(re_height); married.setChecked(re_married); } @Override public void onClick(View view) { String et_name=name.getText().toString(); String et_age=age.getText().toString(); String et_weight=weight.getText().toString(); String et_height=height.getText().toString(); boolean et_married=married.isChecked(); @SuppressLint("CommitPrefEdits") SharedPreferences.Editor editor=sharedPreferences.edit(); editor.putString("name",et_name); editor.putInt("age",Integer.parseInt(et_age)); editor.putFloat("weight",Float.parseFloat(et_weight)); editor.putFloat("height",Float.parseFloat(et_height)); editor.putBoolean("married",et_married); editor.apply(); Toast toast=Toast.makeText(this,"提交成功",Toast.LENGTH_SHORT); toast.show(); } } 可以看到在提交后，会在/data/data/应用程序名中生成一个xml文件。 二、SQLite SQLite与mysql几乎一致，SQLite不支持boolean类型，当输出true和false时会自动转换成0和1. 数据库的打开和删除 package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; public class CreateSqliteDatabase extends AppCompatActivity implements View.OnClickListener { private Button create; private Button delete; private TextView textView; private String DatabaseName; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_create_sqlite_database); create=findViewById(R.id.bt_create); delete=findViewById(R.id.bt_delete); textView=findViewById(R.id.text); create.setOnClickListener(this); delete.setOnClickListener(this); DatabaseName=getFilesDir()+"/test.db"; } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { String desc; switch (view.getId()){ case R.id.bt_create: SQLiteDatabase db=openOrCreateDatabase(DatabaseName,MODE_PRIVATE,null); desc=String.format("数据库%s创建%s",db.getPath(),(db!=null)?"成功":"失败"); textView.setText(desc); break; case R.id.bt_delete: boolean result=deleteDatabase(DatabaseName); desc=String.format("数据库%s删除%s",DatabaseName,result?"成功":"失败"); textView.setText(desc); break; } } } 数据库的增删改查 DatabaseUserHelper.java package com.example.datastorage.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import com.example.datastorage.entity.User; import java.util.ArrayList; import java.util.List; public class DatabaseUserHelper extends SQLiteOpenHelper { private static final String DB_NAME="user.db"; private static final String TABLE_NAME="user_info"; private static final int DB_VERSION=1; private static DatabaseUserHelper databaseUserHelper=null; public DatabaseUserHelper(Context context) { super(context,DB_NAME,null,DB_VERSION); } //单例模式获取数据库实例 public static DatabaseUserHelper getInstance(Context context){ if(databaseUserHelper==null){ databaseUserHelper=new DatabaseUserHelper(context); } return databaseUserHelper; } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { String sql= "CREATE TABLE IF NOT EXISTS "+TABLE_NAME+"(_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,name VARCHAR NOT NULL,age INTEGER NOT NULL,height LONG NOT NULL,weight float NOT NULL,married INTEGER NOT NULL);"; sqLiteDatabase.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { } public Long insert(User user){ SQLiteDatabase db=databaseUserHelper.getWritableDatabase(); ContentValues values=new ContentValues(); values.put("name",user.name); values.put("age",user.age); values.put("weight",user.weight); values.put("height",user.height); values.put("married",user.married); //如果插入的数据是空的,nullColumnHack可以给予一列列名进行插入。 return db.insert(TABLE_NAME,null,values); } public long deleteByName(String name){ SQLiteDatabase db=databaseUserHelper.getWritableDatabase(); return db.delete(TABLE_NAME, "name=?", new String[]{name}); } public long update(User user) { ContentValues values=new ContentValues(); values.put("name",user.name); values.put("age",user.age); values.put("weight",user.weight); values.put("height",user.height); values.put("married",user.married); SQLiteDatabase db = databaseUserHelper.getWritableDatabase(); return db.update(TABLE_NAME,values,"name=?",new String[]{user.name}); } public List<User> queryAll(){ List<User> list=new ArrayList<>(); SQLiteDatabase db=databaseUserHelper.getWritableDatabase(); //返回一个游标 Cursor cursor=db.query(TABLE_NAME,null,null,null,null,null,null); while(cursor.moveToNext()){ User user=new User(); user.id=cursor.getInt(0); user.name=cursor.getString(1); user.age=cursor.getInt(2); user.height=cursor.getLong(3); user.weight=cursor.getFloat(4); user.married= cursor.getInt(5) != 0; list.add(user); } return list; } } User.java类 package com.example.datastorage.entity; import androidx.annotation.NonNull; public class User { public int id; public String name; public int age; public long height; public float weight; public boolean married; public User(String name, int age, long height, float weight, boolean married) { this.name = name; this.age = age; this.height = height; this.weight = weight; this.married = married; } public User(){ } @NonNull @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", height=" + height + ", weight=" + weight + ", married=" + married + '}'; } } xml类: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="5dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="姓名:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的姓名" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="年龄:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/age" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的年龄" android:textSize="17sp" android:inputType="number" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="身高:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/height" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入你的身高" android:textSize="17sp" android:inputType="number" android:textColor="@color/black"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="体重:" android:textSize="17sp" android:textColor="@color/black"/> <EditText android:id="@+id/weight" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入您的体重" android:inputType="number" android:textSize="17sp" android:textColor="@color/black"/> </LinearLayout> <CheckBox android:id="@+id/married" android:layout_width="match_parent" android:layout_height="wrap_content" android:checked="false" android:textColor="@color/black" android:text="已婚" android:textSize="17sp" /> </LinearLayout> Activity类: package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import com.example.datastorage.database.DatabaseUserHelper; import com.example.datastorage.entity.User; import com.example.datastorage.util.ToastUtil; import java.util.List; public class SQLiteHelperActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private Button save; private Button delete; private Button update; private Button query; private DatabaseUserHelper databaseUserHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sqlite_helper); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); save=findViewById(R.id.save); delete=findViewById(R.id.delete); update=findViewById(R.id.update); query=findViewById(R.id.query); save.setOnClickListener(this); delete.setOnClickListener(this); update.setOnClickListener(this); query.setOnClickListener(this); } @Override protected void onStart() { super.onStart(); databaseUserHelper=DatabaseUserHelper.getInstance(this); } @Override protected void onStop() { super.onStop(); //关闭数据库连接 databaseUserHelper.close(); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { String et_name=name.getText().toString(); String et_age=age.getText().toString(); String et_weight=weight.getText().toString(); String et_height=height.getText().toString(); User user=null; switch (view.getId()){ case R.id.save: user=new User(et_name, Integer.parseInt(et_age), Long.parseLong(et_height), Long.parseLong(et_weight), married.isChecked()); if(databaseUserHelper.insert(user)>0){ ToastUtil.show(this,"添加成功"); } break; case R.id.delete: if(databaseUserHelper.deleteByName(et_name)>0){ ToastUtil.show(this,"删除成功"); } case R.id.update: user=new User(et_name, Integer.parseInt(et_age), Long.parseLong(et_height), Long.parseLong(et_weight), married.isChecked()); if(databaseUserHelper.update(user)>0){ ToastUtil.show(this,"更新成功"); } case R.id.query: List<User> list=databaseUserHelper.queryAll(); for (User u :list){ Log.d("query",u.toString()); } break; } } } 事务管理 事务类 beginTransaction：开始事务 setTransactionSuccessful：设置事务成功标志 endTransaction：结束事务 事务的作用就是保证数据的一致性和完整性(避免异常和错误等导致的数据信息异常)，能让多个并发访问数据库的时候彼此不被干扰。 Onupgrade方法 onupgrade方法在数据库版本更新的时候会被执行，即上文的DB_VERSION不为1的时候，数据库在运行的时候会自动比对，不一致则会被执行。 如将版本号变为2，在upgrade中为数据库新添加字段 @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { String sql="ALTER TABLE "+TABLE_NAME+" ADD COLUMN phone VARCHAR"; sqLiteDatabase.execSQL(sql); } 九、外部存储空间 Android把外部存储分成了两块区域，一块是所有应用均可访问的公共空间，另一块是只有应用自己才可以访问的存储空间，私有空间只要应用卸载了 就消失了，但是公共空间即使应用被卸载了，数据依旧会放在里面。 例子：将姓名，身高，年龄，体重，已婚分别保存到不同的外部存储空间，然后将其读取出来，卸载应用后会发现数据依旧存在外部存储空间。 package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import com.example.datastorage.util.FileUtil; import com.example.datastorage.util.ToastUtil; import java.io.File; import java.io.IOException; public class FileWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private TextView tv_text; private String path; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_file_write); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); tv_text=findViewById(R.id.tv_text); Button save = findViewById(R.id.save); Button bt_read= findViewById(R.id.bt_read); save.setOnClickListener(this); bt_read.setOnClickListener(this); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.save: String et_name=name.getText().toString(); String et_age=age.getText().toString(); String et_weight=weight.getText().toString(); String et_height=height.getText().toString(); boolean et_married=married.isChecked(); String stringBuilder = "姓名:" + et_name + "年龄:" + et_age + "身高:" + et_height + "体重:" + et_weight + "婚否:" + (et_married ? "是" : "否"); //外部私有存储空间 String directory=null; String fileName=System.currentTimeMillis()+".txt"; directory=getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).toString(); path=directory+ File.separatorChar+fileName; //外部公共存储空间 // directory=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).toString(); // path=directory+ File.separatorChar+fileName; FileUtil.saveText(path, stringBuilder); ToastUtil.show(this,"保存成功"); Log.d("路径:",path); break; case R.id.bt_read: tv_text.setText(FileUtil.queryText(path)); break; } } } package com.example.datastorage.util; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class FileUtil { public static void saveText(String path,String txt) { BufferedWriter os=null; try { os=new BufferedWriter(new FileWriter(path)); os.write(txt); } catch (Exception e) { e.printStackTrace(); } finally { if(os!=null){ try{ os.close(); } catch (Exception e){ e.printStackTrace(); } } } } public static String queryText(String path){ BufferedReader is=null; StringBuilder stringBuilder=new StringBuilder(); try{ is=new BufferedReader(new FileReader(path)); String line=null; while((line=is.readLine())!=null){ stringBuilder.append(line); } } catch (IOException e) { e.printStackTrace(); } return stringBuilder.toString(); } } 注意存到外部公共空间，需要在Mainifest.xml中加入权限。 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission-sdk-23 android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:requestLegacyExternalStorage="true" android:theme="@style/Theme.MyApplication"> <activity android:name=".FileWriteActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> 存储卡中读取图片 Andorid的位图工具是Bitmap，App读写Bitmap可以使用性能更好的OuputStream和InputStream Android还提供BitmapFactory工具用于读取各种来源图片，方法如下： decodeResource：从资源文件中读取图片信息 decodeFile：该方法可将指定路径图片读取到Bitmap对象 decodeStream：从输入流中读取位图数据 例子：通过bitmap读取图片存储到存储卡中，然后读取到ImageView中显示。 public static void saveImage(String path, Bitmap bitmap) { FileOutputStream outputStream=null; try { outputStream=new FileOutputStream(path); //通过bitmap压缩文件输出流 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); } catch (Exception e){ e.printStackTrace(); } finally { if(outputStream!=null){ try{ outputStream.close(); } catch (Exception e){ e.printStackTrace(); } } } } public static Bitmap openImage(String path){ FileInputStream inputStream=null; Bitmap bitmap=null; try{ inputStream=new FileInputStream(path); bitmap= BitmapFactory.decodeStream(inputStream); }catch (Exception e){ e.printStackTrace(); } finally { if(inputStream!=null){ try{ inputStream.close(); } catch (Exception e){ e.printStackTrace(); } } } return bitmap; } } package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ImageView; import com.example.datastorage.util.FileUtil; import com.example.datastorage.util.ToastUtil; import java.io.File; public class ImageWriteActivity extends AppCompatActivity implements View.OnClickListener{ private ImageView imageView; private String path; @SuppressLint("MissingInflatedId") protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_image_write); Button bt_read=findViewById(R.id.bt_read); Button bt_save=findViewById(R.id.bt_save); imageView=findViewById(R.id.bt_image); bt_read.setOnClickListener(this); bt_save.setOnClickListener(this); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { switch (view.getId()){ case R.id.bt_save: String filename=System.currentTimeMillis()+".jpeg"; path=getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + File.separatorChar +filename; Log.d("图片保存路径",path); Bitmap bitmap= BitmapFactory.decodeResource(getResources(),R.drawable.image); FileUtil.saveImage(path,bitmap); ToastUtil.show(this,"保存成功"); break; case R.id.bt_read: // imageView.setImageBitmap(FileUtil.openImage(path)); Bitmap bitmap1=BitmapFactory.decodeFile(path); imageView.setImageBitmap(bitmap1); break; } } } Application Application是Android的一大组件，在App的运行过程中有且只有一个Application对象，贯穿整个生命周期，创建在Acitivity之前。 Application有如下的三种方法： onCreate()，启动程序时调用。 onTerminate()，App退出时调用，用于模拟环境测试，不会在真实产品中回调。 OnConfigurationChange()，配置改变时调用，如竖屏变横屏。 一般用于读取一些要频繁读取的小型数据，因为存放的位置是内存，因此不能放太多太大的数据。 例子：将一个人的信息存放到Application变量中。 MyApplication.java类: package com.example.datastorage; import android.app.Application; import android.util.Log; import java.util.HashMap; public class MyApplication extends Application { private static MyApplication application; public HashMap<String,String> stringHashMap=new HashMap<>(); public static MyApplication getInstance(){ return application; } @Override public void onCreate() { super.onCreate(); application=this; Log.d("App状态:","App启动"); } } package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import java.util.Objects; public class AppWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText age; private EditText weight; private EditText height; private CheckBox married; private Button save; private MyApplication myApplication; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_app_write); name=findViewById(R.id.name); age=findViewById(R.id.age); weight=findViewById(R.id.weight); height=findViewById(R.id.height); married=findViewById(R.id.married); save=findViewById(R.id.save); save.setOnClickListener(this); myApplication=MyApplication.getInstance(); reload(); } private void reload() { String app_name=myApplication.stringHashMap.get("name"); if(app_name==null){ return ; } String app_age= myApplication.stringHashMap.get("age"); String app_weight=myApplication.stringHashMap.get("weight"); String app_height=myApplication.stringHashMap.get("height"); String app_married=myApplication.stringHashMap.get("married"); name.setText(app_name); age.setText(app_age); weight.setText(app_weight); height.setText(app_height); if(Objects.equals(app_married, "是")){ married.setChecked(true); } else{ married.setChecked(false); } } @Override public void onClick(View view) { String et_name=name.getText().toString(); String et_age=age.getText().toString(); String et_weight=weight.getText().toString(); String et_height=height.getText().toString(); boolean et_married=married.isChecked(); myApplication.stringHashMap.put("name",et_name); myApplication.stringHashMap.put("age",et_age); myApplication.stringHashMap.put("weight",et_weight); myApplication.stringHashMap.put("height",et_height); myApplication.stringHashMap.put("married",et_married ?"是":"否"); } } 只要程序没有被关闭，Application存放的东西就依旧存在，程序关闭后，数据被回收就不存在了。 十、Jetpack Room Room框架基于SQLite，通过注解技术极大的简化了数据库操作，减少了原来相当一部分的代码量，跟Java后端开发的Mybatis大致相同，就是可以直接使用注解完成数据库的操作。 使用Room之前要修改build.grandle模块文件，在dependencies中添加两行： implementation 'androidx.room:room-runtime:2.5.1' annotationProcessor 'androidx.room:room-compiler:2.5.1' 例子：写一个书籍数据库的增删改查 Room框架编码过程一般分为以下五步： @Entity：编写书籍信息表对应的实体类 @Dao：编写书籍信息表对应的持久化类 @Database：编写书籍信息表对应的数据库类，由RoomDatabase派生 在自定义的Application类中声明数据库唯一实例 在操作书籍信息表的地方获取数据表的持久化对象 创建数据库实体类 package com.example.datastorage.entity; import androidx.room.Entity; import androidx.room.PrimaryKey; @Entity public class Book { @PrimaryKey(autoGenerate = true) public Integer id; public String name; public String author; public String press; public String price; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getPress() { return press; } public void setPress(String press) { this.press = press; } public String getPrice() { return price; } public void setPrice(String price) { this.price = price; } @Override public String toString() { return "Book{" + "id=" + id + ", name='" + name + '\'' + ", author='" + author + '\'' + ", press='" + press + '\'' + ", price='" + price + '\'' + '}'; } } 创建Dao接口 package com.example.datastorage.Dao; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; import androidx.room.Update; import com.example.datastorage.entity.Book; import java.util.List; @Dao public interface BookDao { @Insert void insert(Book... books); @Delete void delete(Book...books); @Query("DELETE FROM Book") void deleteAll(); @Update int update(Book...books); @Query("SELECT *FROM Book") List<Book> queryAll(); @Query("SELECT *FROM Book where name=:name order by id desc limit 1") Book queryByName(String name); } 持久化类 package com.example.datastorage.database; import androidx.room.Database; import androidx.room.RoomDatabase; import com.example.datastorage.Dao.BookDao; import com.example.datastorage.entity.Book; //exportSchema表示是否导出数据库信息的json串(便于检查调试)，设为true需要gradle指定json的生成路径。 @Database(entities = {Book.class},version = 1,exportSchema = false) public abstract class BookDatabase extends RoomDatabase { //获得数据库某张表的持久化对象 public abstract BookDao bookDao(); } Application启动类： package com.example.datastorage; import android.app.Application; import android.util.Log; import androidx.room.Room; import com.example.datastorage.database.BookDatabase; import java.util.HashMap; public class MyApplication extends Application { private static MyApplication application; public HashMap<String,String> stringHashMap=new HashMap<>(); public static MyApplication getInstance(){ return application; } private BookDatabase bookDatabase; @Override public void onCreate() { super.onCreate(); application=this; Log.d("App状态:","App启动"); //构建书籍数据库实例,允许数据库迁移和主线程运行 bookDatabase= Room.databaseBuilder(this,BookDatabase.class, "book").addMigrations() .allowMainThreadQueries() .build(); } public BookDatabase getBookDB(){ return bookDatabase; } } 使用类： package com.example.datastorage; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import com.example.datastorage.Dao.BookDao; import com.example.datastorage.entity.Book; import com.example.datastorage.util.ToastUtil; import java.util.List; public class RoomWriteActivity extends AppCompatActivity implements View.OnClickListener { private EditText name; private EditText Author; private EditText press; private EditText price; private BookDao bookDao; @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_room_write); name=findViewById(R.id.bookName); Author=findViewById(R.id.author); press=findViewById(R.id.press); price=findViewById(R.id.price); Button bt_save=findViewById(R.id.save); Button bt_query=findViewById(R.id.query); Button bt_delete=findViewById(R.id.delete); Button bt_update=findViewById(R.id.update); bt_save.setOnClickListener(this); bt_query.setOnClickListener(this); bt_delete.setOnClickListener(this); bt_update.setOnClickListener(this); bookDao=MyApplication.getInstance().getBookDB().bookDao(); } @SuppressLint("NonConstantResourceId") @Override public void onClick(View view) { String put_name=name.getText().toString(); String put_author=Author.getText().toString(); String put_press=press.getText().toString(); String put_price=price.getText().toString(); switch (view.getId()){ case R.id.save: Book book=new Book(); book.setName(put_name); book.setAuthor(put_author); book.setPress(put_press); book.setPrice(put_price); bookDao.insert(book); ToastUtil.show(this,"保存成功"); break; case R.id.delete: Book b2=new Book(); b2.setId(2); bookDao.delete(b2); ToastUtil.show(this,"删除成功"); break; case R.id.query: List<Book> list=bookDao.queryAll(); for(Book b : list){ Log.d("查询书籍：", b.toString()); } break; case R.id.update: Book b4=bookDao.queryByName(put_name); Book b3=new Book(); b3.setId(b4.getId()); b3.setName(put_name); b3.setAuthor(put_author); b3.setPress(put_press); b3.setPrice(put_price); bookDao.update(b3); ToastUtil.show(this,"修改成功"); break; } } } 十一、正文字数太多，开启下一篇
Sringboot2整合shiro实现登录认证、权限控制和记住我功能
前言 很久没写过文章了，最近课程设计硬性要求要使用Spring Secure或shiro其中一种安全框架，因为shiro的轻便更被广泛使用，所以学了一下shiro框架的基本使用，也来了点兴致，所以记录一下。 提示：以下是本篇文章正文内容，下面案例可供参考 一、关于Shiro Apache Shiro 是一个功能强大且易于使用的Java安全框架，为开发人员提供了一种直观而全面的身份验证，授权，加密和会话管理解决方案，提供了身份验证、授权、密码学、会话管理四大基本安全功能、还提供一些缓存、记住我、并发等多种额外功能解决一些问题。 Shiro的一些核心概念有：Realms、SecutiryManager、Subjuect。 关于Subject可直接理解为对象，用于和应用进行交互，通过Subject传入到安全管理器。 关于SecurityManger就是安全管理器，所有的安全性操作都与它进行交互，是Shiro管理的核心。 关于Realms可以理解为存储域，存储Shiro的一些安全数据等，安全管理器需要从Realms获取数据，验证是否合法。 大概就这些，学的比较粗浅，底层源码也没咋了解，直接就学了如何使用。 二、功能实现 1.创建User表的数据库 实现Shiro与数据库交互，能够通过手机号+密码或邮箱+密码进行登录验证。 这里的数据库字段设置如上、Id号（主键）、name（姓名）、phone（手机号）、username（用户名）、password（密码）、email（邮箱）、state（状态）。 2.引入依赖 pom.xml文件如下： <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.4</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>shiroDemo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>shiroDemo</name> <description>shiroDemo</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.17</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.10.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.10.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> 3.配置application.yaml文件 当然也可以配置application.properties，个人习惯使用yaml，这里配置mybatis和连接mysql数据库，如下： spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/teamwork?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root type: com.alibaba.druid.pool.DruidDataSource mybatis: #config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置 mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置 configuration: map-underscore-to-camel-case: true 4.完善Mybatis 创建User类如下： package com.example.shirodemo.Bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private Integer id; private String name; private String phone; private String username; private String password; private String email; private Integer state; } 创建Mapper接口和UserMapper.xml文件 <?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="com.example.shirodemo.mapper.UserMapper"> </mapper> package com.example.shirodemo.mapper; import com.example.shirodemo.Bean.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; @Mapper public interface UserMapper { @Select("select *from user where password=#{password} limit 1") User ShiroAuthority(String password); @Select("select password from user where phone=#{phone} ") User Login(String phone); @Select("select password from user where email=#{email}") User LoginByEmail(String email); } 这里用两个Select语句，当前端传入数据第一种方式查询不到user用户时，就进行第二种，以此来实现两种登录方式，当然也可以一个方法直接写入到XML文件中，比如下面这样。 <?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="com.example.homework1.mapper.UserMapper"> select *from User <where> <if test="phone!=null and phone!=''"> and phone=#{phone} </if> <if test="email!=null and email!=''"> and email=#{email} </if> and password= #{password} </where> 5.重写Realm和Shiro配置类 因为数据库中存储的密码是用的base64+md5加密存储，所以这里要先写一个MD5的加密类，如下： package com.example.shirodemo.config; import org.springframework.util.DigestUtils; import java.util.Base64; import static com.mysql.cj.util.StringUtils.getBytes; public class MD5 { /** * @param text明文 * @param key密钥 * @return 密文 * 对数据库内的密码存储进行md5的加密 */ public static String md5(String password,String key) throws Exception { String base64encodedString = Base64.getEncoder().encodeToString((password + key).getBytes("utf-8")); // 加密后的字符串 return DigestUtils.md5DigestAsHex(getBytes(base64encodedString)); } } 重写Realm方法 package com.example.shirodemo.config; import com.example.shirodemo.Bean.User; import com.example.shirodemo.mapper.UserMapper; import com.example.shirodemo.service.UserService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import java.util.Collections; public class MyRealm extends AuthorizingRealm { @Autowired UserService userService; @Autowired UserMapper userMapper; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { User user = (User) principals.getPrimaryPrincipal(); if (user != null) { User users = (User) userMapper.ShiroAuthority(user.getPassword()); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();//权限信息 if (users.getUsername().equals("admin")) { System.out.println(users.getName()); simpleAuthorizationInfo.addRole("admin"); } else { simpleAuthorizationInfo.addRole("user"); } return simpleAuthorizationInfo; } return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) { String username= (String) authcToken.getPrincipal(); // 获取用户密码 User user; user = userService.Login(username); if (user == null) { // 没找到帐号 user =userService.LoginByEmail(username); if(user==null) throw new UnknownAccountException(); } // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配 return new SimpleAuthenticationInfo(user, user.getPassword(),getName()); } } 这里主要重写两个方法，第一个方法用于进行权限验证，这里用不到，下面一个方法用于进行登录验证，主要从Token中取得用户名，然后是否存在此用户，不存在则报错，存在则交给CredentialsMatcher进行密码进行密码匹配，principals用于控制角色，通过Autoken中的对象查询数据库的用户名，判断是否是admin，添加角色用于判断。 ShiroConfiguration类 package com.example.shirodemo.config; import org.apache.shiro.codec.Base64; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfiguration { @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Shiro生命周期处理器 * @return */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ return new LifecycleBeanPostProcessor(); } /** * 自动创建代理 * @return */ @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } /** 解决MyRealm中UserMapper一直是null的问题 **/ @Bean MyRealm myRealm() { return new MyRealm(); } /** *安全管理器 * **/ @Bean SecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm()); manager.setRememberMeManager(rememberMeManager()); manager.setSessionManager(sessionManager()); return manager; } @Bean ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //指定 SecurityManager bean.setSecurityManager(securityManager()); //登录页面 bean.setLoginUrl("/login"); //登录成功页面 bean.setSuccessUrl("/"); //访问未获授权路径时跳转的页面 bean.setUnauthorizedUrl("/login"); //配置路径拦截规则，注意，要有序 Map<String, String> map = new LinkedHashMap<>(); map.put("/doLogin", "anon"); map.put("/logout","logout"); map.put("/**", "user"); bean.setFilterChainDefinitionMap(map); return bean; } /** 但是再shiro:1.6开始，新增了一个InvalidRequestFilter的过滤器，用于拦截存在安全问题的uri，不进行配置在首次访问/时，URL中会出现jsessionid，并返回400错误 * **/ @Bean public DefaultWebSessionManager sessionManager(){ DefaultWebSessionManager sessionManager=new DefaultWebSessionManager(); sessionManager.setSessionIdUrlRewritingEnabled(false); return sessionManager; } /**设置Cookie**/ @Bean public SimpleCookie rememberMeCookie() { SimpleCookie cookie = new SimpleCookie("rememberMe"); cookie.setMaxAge(86400); return cookie; } /** * 设置cookie管理对象和Cookie的加密密钥，默认是AES加密 * @return */ @Bean public CookieRememberMeManager rememberMeManager() { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag==")); return cookieRememberMeManager; } } 将logout写入filterChainDefinitionMap链中就可以自动实现注销功能，注意UserMapper类和跳转被拦截的问题即可，CookieRememberMeManager和 SimpleCookie类主要用于记住我功能后生成返回Cookie，前面三个类使@RequireRole()注解的角色控制生效。 6.前端和Controller层 Controller层——IndexController package com.example.shirodemo.Controller; import com.example.shirodemo.Bean.User; import com.example.shirodemo.config.MD5; import com.example.shirodemo.mapper.UserMapper; import com.example.shirodemo.service.UserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @Controller public class IndexController { @GetMapping("/login") public String login(){ return "login"; } @GetMapping("/") public String index(Model model) { model.addAttribute("message", "登录成功"); return "index"; } @PostMapping("/doLogin") public String doLogin(String username, String password, boolean rememberMe,Model model){ Subject subject= SecurityUtils.getSubject(); try { String surepassword= MD5.md5(password,"Aiwin"); UsernamePasswordToken token=new UsernamePasswordToken(username, surepassword); token.setRememberMe(rememberMe); subject.login(token); model.addAttribute("message","登录成功"); return "index"; } catch (AuthenticationException e) { e.printStackTrace(); model.addAttribute("message","登录失败"); return "login"; } catch (Exception e) { e.printStackTrace(); return "login"; } } } 主要是Suject类，将接受到的用户和正确的密码提交到Token，通过与Subject类进行交互进行login，MyRealm中取得的用户名和密码可以理解为就是从这里Token交上去的。 HomeController类: package com.example.shirodemo.Controller; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.annotation.RequiresRoles; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HomeController { @GetMapping("/home") public String home(){ return "hello"; } @RequiresRoles("admin") @GetMapping("/third") public String third(){ return "third"; } } 前端页面 Index.html: Title 注销 login.html Title 注销 跳转 跳转管理员页面 hello.html 第二个页面 这是第二个页面 third.html 第三个页面 只能管理员访问哦 至此，功能全部实现，实现效果图如下 直接关闭浏览器，然后再访问/，可以直接跳转到登录成功的页面，点击注销再访问则会跳转到登录页面，应该是通过Cookie中的rememberMe中取得user的信息进行验证判断是否通过。