物联网网关Web服务器--CGI开发实例BMI计算
本例子通一个计算体重指数的程序来演示Web服务器CGI开发。
硬件环境:飞腾派开发板(国产E2000处理器)
软件环境:飞腾派OS(Phytium Pi OS)
硬件平台参考另一篇博客:国产化ARM平台-飞腾派开发板硬件与系统
lighttpd服务器部署参考另一篇博客:物联网网关Web服务器--lighttpd服务器部署与应用测试
1、部署与运行效果
//启动服务器
user@phytiumpi:/var/www$ sudo service lighttpd start//服务器根目录/var/www部署如下目录与文件
user@phytiumpi:/var/www$ tree
.
`-- html|-- bmi.png|-- bmi_index.png|-- cgi-bin| `-- bmi.cgi`-- index.html2 directories, 4 files//文件权限如下
user@phytiumpi:/var/www$ ls -lh html/*
-rw-r--r-- 1 root root 36K Jan 16 11:07 html/bmi.png
-rw-r--r-- 1 root root 13K Jan 16 14:08 html/bmi_index.png
-rw-r--r-- 1 root root 688 Jan 16 11:07 html/index.htmlhtml/cgi-bin:
total 16K
-rwxr-xr-x 1 root root 15K Jan 16 13:51 bmi.cgi//bmi.cgi文件类型
user@phytiumpi:/var/www$ file html/cgi-bin/bmi.cgi
html/cgi-bin/bmi.cgi: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=f39d7a4a7551ef3e3b4eba59a12959d4bc636032, for GNU/Linux 3.7.0, not stripped
-
浏览器运行
输入身高与体重信息,点击“计算”按钮,会提交当前网页中的表单数据到Web服务器并返回计算后的BMI数据。

2、Index网页文件说明
index.html是web服务器默认的页面文件,主要作用就是显示一个静态页面,提交当前页面后指定的cgi程序执行处理。
//action 指定了cgi-bin\bmi.cgi为提交后执行的程序文件
<form action="cgi-bin\bmi.cgi" method="get">
index.html源码:
<html>
<body>
<div align="center">
<form action="cgi-bin\bmi.cgi" method="get"> <table> <tr><td rowspan="3"><img src="bmi.png" hight="60" width="120"></td><td align="center" colspan="3"><h2>体重指数(BMI)计算器</h2></td></tr><tr><td >身高 : <input type="number" name="cm" min="1" max="300" size="3"> cm </td><td >体重 : <input type="number" name="kg" min="1" max="500" size="3"> kg </td><td align="center" ><input type=submit value=" 计 算 " size="16"> </td></tr><tr><td align="center" colspan="3">BMI = <input type="text" name="ret" value="" size="3" readonly></tr> </table>
</form>
</br><img src=bmi_index.png >
</div>
</body>
</html>
3、HTTP 请求处理功能说明
通过getvalue.h头文件实现。
宏定义和全局变量
#define FIELD_LEN 60
#define NV_PAIRS 15 typedef struct name_value_st{char name[FIELD_LEN + 1];char value[FIELD_LEN + 1];
} name_value;name_value name_val_pairs[NV_PAIRS];
int num_pairs = 0;/*pairs number*/
const char *M = NULL;
const char *L = NULL;
const char *S = NULL;
static int iread = 0;
-
FIELD_LEN宏定义了每个名称或值的最大长度为 60。 -
NV_PAIRS宏定义了可以处理的名称 - 值对的最大数量为 15。 -
name_value结构体包含两个字符数组name和value,分别用于存储名称和值,长度为FIELD_LEN + 1。 -
name_val_pairs是name_value结构体的数组,用于存储多个名称 - 值对。 -
num_pairs用于记录实际存储的名称 - 值对的数量。 -
M、L、S是指向常量字符的指针,初始化为NULL,可能用于存储请求方法、内容长度和查询字符串。 -
iread是静态整型变量,可能用于记录读取值的次数。
函数声明
void unescape_url(char *url);
void set_env(const char *r_mth, const char *c_len,const char *q_str);
char* get_value(const char *name);
int get_input(void);
void send_error(char *error_test);
char x2c(char *what);
void load_nv_pair(char *tmp_buffer, int nv_entry_number_to_load);
-
unescape_url(char *url):对 URL 进行转义处理。 -
set_env(const char *r_mth, const char *c_len,const char *q_str):设置环境变量,将传入的三个参数存储到全局指针M、L和S中。 -
get_value(const char *name):根据传入的名称查找并返回对应的value。 -
get_input(void):获取输入数据,根据请求方法(POST 或 GET)将数据存储在ip_data中,并将数据解析为名称 - 值对存储在name_val_pairs中。 -
send_error(char *error_text):输出错误信息,以 HTML 格式输出错误信息。 -
x2c(char *what):将十六进制表示的字符转换为对应的 ASCII 字符。 -
load_nv_pair(char *tmp_buffer, int nv_entry_number_to_load):将tmp_buffer中的名称 - 值对加载到name_val_pairs数组的指定条目中。
set_env(const char *r_mth, const char *c_len,const char *q_str)
void set_env(const char *r_mth, const char *c_len,const char *q_str)
{M = r_mth;L = c_len;S = q_str;
}
-
功能:将传入的三个参数
r_mth、c_len和q_str分别赋值给全局指针M、L和S,用于存储环境信息。
get_value(const char *name)
char* get_value(const char *name)
{int nv_entry_number = 0;int i = 0;char* val = NULL;char *tname = NULL;if(iread == 0){ if (!get_input()){return "error";exit(EXIT_FAILURE);}}for(i = 0; i < num_pairs; i++ ){ val = name_val_pairs[nv_entry_number].value;tname = name_val_pairs[nv_entry_number].name;nv_entry_number++;if( strcmp(tname,name) == 0 ){ break;}else{ val = NULL; tname = NULL;}}iread++;//read value timesreturn val; exit(EXIT_SUCCESS);
}
-
功能:
-
首先,如果
iread为 0,则调用get_input()函数获取输入数据。如果get_input()失败,返回"error"并终止程序。 -
然后遍历
name_val_pairs数组,比较每个名称 - 值对的名称部分和传入的name,如果匹配,将对应的value存储在val中。 -
增加
iread的值,表示读取值的次数。 -
最后返回找到的
value,如果未找到,返回NULL。
-
get_input(void)
int get_input(void)
{int nv_entry_number = 0;int got_data = 0;char *ip_data = NULL;int ip_length = 0;char tmp_buffer[(FIELD_LEN * 2) + 2];int tmp_offset = 0;char *tmp_char_ptr = NULL;int chars_processed = 0;tmp_char_ptr = (char*)M;if ( tmp_char_ptr){if(strcmp(tmp_char_ptr, "POST") == 0){tmp_char_ptr = (char*)L;if (tmp_char_ptr){ip_length = atoi(tmp_char_ptr);ip_data = malloc(ip_length + 1);if (fread(ip_data, 1, ip_length, stdin)!= ip_length){send_error("Bad read from stdin");return(0);}ip_data[ip_length] = '\0';got_data = 1;}}}tmp_char_ptr = (char*)M;if ( tmp_char_ptr){if(strcmp(tmp_char_ptr, "GET") == 0){tmp_char_ptr = (char*)S;if (tmp_char_ptr){ip_length = strlen(tmp_char_ptr);ip_data = malloc(ip_length + 1);strcpy(ip_data, (char*)S);ip_data[ip_length] = '\0';got_data = 1;}}}if (!got_data){send_error("No data received");}if (ip_length <= 0){send_error("Input length <= 0");return(0);}memset(name_val_pairs, '\0', sizeof(name_val_pairs));tmp_char_ptr = ip_data;while (chars_processed <= ip_length && nv_entry_number < NV_PAIRS){tmp_offset = 0;while (*tmp_char_ptr && *tmp_char_ptr!= '&' && tmp_offset < FIELD_LEN){tmp_buffer[tmp_offset] = *tmp_char_ptr;tmp_offset++;tmp_char_ptr++;chars_processed++;}tmp_buffer[tmp_offset] = '\0';load_nv_pair(tmp_buffer, nv_entry_number);tmp_char_ptr++;nv_entry_number++;}free(ip_data);ip_data = NULL;return(1);
}
-
功能:
-
首先,通过
M检查请求方法。 -
如果是
POST方法,通过L获取内容长度,分配足够的内存给ip_data,使用fread从标准输入读取数据,处理读取错误。 -
如果是
GET方法,通过S获取查询字符串,分配内存给ip_data,复制查询字符串,添加字符串结束符。 -
检查是否有数据,如果没有数据,调用
send_error函数报错。 -
检查输入长度是否小于等于 0,若是则报错。
-
清空
name_val_pairs数组。 -
遍历
ip_data,将数据存储在tmp_buffer中,遇到&符号或达到FIELD_LEN长度时,调用load_nv_pair函数将数据存储到name_val_pairs数组中。 -
释放
ip_data的内存。
-
send_error(char *error_text)
void send_error(char *error_text)
{printf("Content-Type: text/html\r\n");printf("\r\n");printf("Woops:- %s\r\n",error_text);
}
-
功能:输出 HTML 头信息和错误信息,用于向用户反馈错误信息。
load_nv_pair(char *tmp_buffer, int nv_entry)
void load_nv_pair(char *tmp_buffer, int nv_entry)
{int chars_processed = 0;char *src_char_ptr = NULL;char *dest_char_ptr = NULL;src_char_ptr = tmp_buffer;dest_char_ptr = name_val_pairs[nv_entry].name;while (*src_char_ptr && *src_char_ptr!= '=' && chars_processed < FIELD_LEN){if (*src_char_ptr == '+'){*dest_char_ptr = ' ';}else{*dest_char_ptr = *src_char_ptr;}dest_char_ptr++;src_char_ptr++;chars_processed++;}if (*src_char_ptr == '='){num_pairs++;src_char_ptr++;dest_char_ptr = name_val_pairs[nv_entry].value;chars_processed = 0;while (*src_char_ptr && *src_char_ptr!= '=' && chars_processed < FIELD_LEN){if (*src_char_ptr == '+'){*dest_char_ptr = ' ';}else{*dest_char_ptr = *src_char_ptr;}dest_char_ptr++;src_char_ptr++;chars_processed++;}}unescape_url(name_val_pairs[nv_entry].name);unescape_url(name_val_pairs[nv_entry].value);
}
-
功能:
-
将
tmp_buffer中的数据解析为名称 - 值对,将名称存储在name_val_pairs[nv_entry].name中,将值存储在name_val_pairs[nv_entry].value中。 -
将
+替换为空格。 -
调用
unescape_url函数对名称和值进行 URL 转义处理。
-
unescape_url(char *url)
void unescape_url(char *url)
{int x,y;for (x=0,y=0; url[y]; ++x,++y ){if ( (url[x] = url[y]) == '%'){url[x] = x2c(&url[y+1]);y += 2;}}url[x] = '\0';
}
-
功能:
-
遍历
url字符串。 -
当遇到
%时,调用x2c函数将后面的两个字符转换为对应的 ASCII 字符。
-
x2c(char *what)
char x2c(char *what)
{register char digit;digit = (what[0] >= 'A'? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));digit *= 16;digit += (what[1] >= 'A'? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));return(digit);
}
-
功能:将十六进制表示的字符(如
%xx)转换为对应的 ASCII 字符。
4、应用功能主程序说明
通过bmi.c程序实现。关键代码分析说明如下:
set_env(getenv("REQUEST_METHOD"),getenv("CONTENT_LENGTH"),getenv("QUERY_STRING"));
-
getenv("REQUEST_METHOD"):该函数用于获取名为REQUEST_METHOD的环境变量的值。在 HTTP 服务器环境中,REQUEST_METHOD通常包含请求的方法,例如GET、POST、PUT等。 -
getenv("CONTENT_LENGTH"):该函数用于获取名为CONTENT_LENGTH的环境变量的值。在 HTTP 请求中,如果是POST方法,CONTENT_LENGTH表示请求体的长度。 -
getenv("QUERY_STRING"):该函数用于获取名为QUERY_STRING的环境变量的值。在 HTTP 的GET请求中,QUERY_STRING包含了 URL 中的查询部分(即?后面的部分)。
val_cm = get_value("cm"); val_kg = get_value("kg");
-
通过getvalue.h文件中的自定义函数get_value,可根据传入的名称查找并返回对应的字符串值。
bmi.c文件源码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include "getvalue.h"int main(int argc, char *argv[])
{char *val_cm = NULL;char *val_kg = NULL;int cm,kg,len;float cm_f=0.0,kg_f=0.0,bmi=0.0;set_env(getenv("REQUEST_METHOD"),getenv("CONTENT_LENGTH"),getenv("QUERY_STRING")); val_cm = get_value("cm"); val_kg = get_value("kg"); cm = atoi(val_cm);kg = atoi(val_kg);cm_f = (cm + 0.0) / 100;kg_f = kg + 0.0;bmi = kg_f/(cm_f*cm_f);//计算bmi数值//下面输出信息都是HTML文件输出printf( "Content-type:text/html\n\n" ); printf("<html><body><div align=\"center\">\n");//输出调试信息printf("Debug INFO:CM=%f,KG=%f,BMI=%f \n",cm_f,kg_f,bmi);printf("<form action=\"bmi.cgi\" method=\"get\"><table> \n");printf("<tr><td rowspan=\"3\"><img src=\"../bmi.png\" hight=\"60\" width=\"120\"></td> \n");printf("<td align=\"center\" colspan=\"3\"><h2>体重指数(BMI)计算器</h2></td></tr> \n");printf("<tr><td >身高 : <input type=\"number\" name=\"cm\" min=\"1\" max=\"300\" size=\"3\"> cm </td> \n");printf("<td >体重 : <input type=\"number\" name=\"kg\" min=\"1\" max=\"500\" size=\"3\"> kg </td> \n");printf("<td align=\"center\" ><input type=submit value=\" 计 算 \" size=\"16\"> </td></tr> \n");printf("<tr><td align=\"center\" colspan=\"3\">BMI = \n");if(bmi == 0.0)printf("<input type=\"text\" name=\"ret\" value=\" \" size=\"3\" readonly></tr> \n");elseprintf("<input type=\"text\" name=\"ret\" value=\" %.1f \" size=\"3\" readonly></tr> \n",bmi); printf("</table></form></br><img src=\"../bmi_index.png\" > \n");printf("</div></body> </html> \n");return 0;
}
相关文章:
物联网网关Web服务器--CGI开发实例BMI计算
本例子通一个计算体重指数的程序来演示Web服务器CGI开发。 硬件环境:飞腾派开发板(国产E2000处理器) 软件环境:飞腾派OS(Phytium Pi OS) 硬件平台参考另一篇博客:国产化ARM平台-飞腾派开发板…...
计算机网络 (51)鉴别
前言 计算机网络鉴别是信息安全领域中的一项关键技术,主要用于验证用户或信息的真实性,以及确保信息的完整性和来源的可靠性。 一、目的与重要性 鉴别的目的是验明用户或信息的正身,对实体声称的身份进行唯一识别,以便验证其访问请…...
【Docker】搭建一个功能强大的自托管虚拟浏览器 - n.eko
前言 本教程基于群晖的NAS设备DS423的docker功能进行搭建,DSM版本为 DSM 7.2.2-72806 Update 2。 n.eko 支持多种类型浏览器在其虚拟环境中运行,本次教程使用 Chromium 浏览器镜像进行演示,支持访问内网设备和公网地址。 简介 n.eko 是…...
论文笔记(六十二)Diffusion Reward Learning Rewards via Conditional Video Diffusion
Diffusion Reward Learning Rewards via Conditional Video Diffusion 文章概括摘要1 引言2 相关工作3 前言4 方法4.1 基于扩散模型的专家视频建模4.2 条件熵作为奖励4.3 训练细节 5 实验5.1 实验设置5.2 主要结果5.3 零样本奖励泛化5.4 真实机器人评估5.5 消融研究 6 结论 文章…...
探索 Stable-Diffusion-Webui-Forge:更快的AI图像生成体验
目录 简介🌟 主要特点📥 安装步骤1. 下载2. 配置环境和安装依赖3. 模型目录说明 🚀 运行 Stable-Diffusion-Webui-Forge1. 进入项目目录2. 运行项目3. 打开页面 🎨 使用体验常见问题📝 小结 简介 Stable-Diffusion-We…...
Redis使用基础
1 redis介绍 Redis(Remote Dictionary Server ),即远程字典服务 ! 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库。 使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并…...
PyCharm+RobotFramework框架实现UDS自动化测试- (四)项目实战0x10
1.环境搭建 硬件环境:CANoe、待测设备(包含UDS诊断模块) 2.pythonPyCharm环境 pip install robotframework pip install robotframework-ride pip install openpyxl pip install udsoncan pip install python-can pip install can-isotp3…...
【TCP】rfc文档
tcp协议相关rfc有哪些 TCP(传输控制协议)是一个复杂的协议,其设计和实现涉及多个RFC文档。以下是一些与TCP协议密切相关的RFC文档列表,按照时间顺序排列,涵盖了从基础定义到高级特性和优化的各个方面: 基…...
【SpringCloud】黑马微服务学习笔记
目录 1. 关于微服务 ?1.1 微服务与单体架构的区别 ?1.2 SpringCloud 技术 2. 学习前准备 ?2.1 环境搭建 ?2.2 熟悉项目 3. 正式拆分 ?3.1 拆分商品功能模块 ?3.2 拆分购物车功能模块 4. 服务调用 ?4.1 介绍 ?4.2 RustTemplate?的使用 4.3 服务治理-注册中…...
梯度提升决策树树(GBDT)公式推导
### 逻辑回归的损失函数 逻辑回归模型用于分类问题,其输出是一个概率值。对于二分类问题,逻辑回归模型的输出可以表示为: \[ P(y 1 | x) \frac{1}{1 e^{-F(x)}} \] 其中 \( F(x) \) 是一个线性组合函数,通常表示为ÿ…...
【MySQL】表的基本操作
??表的基本操作 文章目录: 表的基本操作 创建查看表 创建表 查看表结构 表的修改 表的重命名 表的添加与修改 删除表结构 总结 前言: 在数据库中,数据表是存储和组织数据的基本单位,对于数据表的操作是每个程序员需要烂熟…...
项目中使用的是 FastJSON(com.alibaba:fastjson)JSON库
从你的 pom.xml 文件中可以看到,项目明确依赖了以下 JSON 库: FastJSON: <dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version> </depende…...
Flutter中PlatformView在鸿蒙中的使用
Flutter中PlatformView在鸿蒙中的使用 概述在Flutter中的处理鸿蒙端创建内嵌的鸿蒙视图创建PlatformView创建PlatformViewFactory创建plugin,注册platformview注册插件 概述 集成平台视图(后称为平台视图)允许将原生视图嵌入到 Flutter 应用…...
音频入门(一):音频基础知识与分类的基本流程
音频信号和图像信号在做分类时的基本流程类似,区别就在于预处理部分存在不同;本文简单介绍了下音频处理的方法,以及利用深度学习模型分类的基本流程。 目录 一、音频信号简介 1. 什么是音频信号 2. 音频信号长什么样 二、音频的深度学习分…...
规避路由冲突
路由冲突是指在网络中存在两个或多个路由器在进行路由选择时出现矛盾,导致网络数据包无法正确传输,影响网络的正常运行。为了规避路由冲突,可以采取以下措施: 一、合理规划IP地址 分配唯一IP:确保每个设备在网络中都有…...
SQLmap 自动注入 -02
1: 如果想获得SQL 数据库的信息,可以加入参数: -dbs sqlmap -u "http://192.168.56.133/mutillidae/index.php?pageuser-info.php&usernamexiaosheng&passwordabc&user-info-php-submit-buttonViewAccountDetails" --batch -p username -dbs…...
4.JoranConfigurator解析logbak.xml
文章目录 一、前言二、源码解析GenericXMLConfiguratorlogback.xml解析通过SaxEvent构建节点model解析model节点DefaultProcessor解析model 三、总结 一、前言 上一篇介绍了logback模块解析logback.mxl文件的入口, 我们可以手动指定logback.xml文件的位置, 也可以使用其它的名…...
React 19 新特性总结
具体详见官网: 中文:React 19 新特性 英文:React 19 新特性 核心新特性 1. Actions 解决问题:简化数据变更和状态更新流程 以前需要手动处理待定状态、错误、乐观更新和顺序请求需要维护多个状态变量(isPending, error 等) 新…...
kafka学习笔记6 ACL权限 —— 筑梦之路
在Kafka中,ACL(Access Control List)是用来控制谁可以访问Kafka资源(如主题、消费者组等)的权限机制。ACL配置基于Kafka的kafka-acls.sh工具,能够管理对资源的读取、写入等操作权限。 ACL介绍 Kafka的ACL是…...
【Java】Java抛异常到用户界面公共封装
前言 在Java中处理代码运行异常是常见的技术点之一,我们大部分会使用封装的技巧将异常进行格式化输出,方便反馈给用户界面,也是为了代码复用 看看这行代码是怎么处理异常的 CommonExceptionType.SimpleException.throwEx("用户信息不…...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...
AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...
LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
【Go】3、Go语言进阶与依赖管理
前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课,做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程,它的核心机制是 Goroutine 协程、Channel 通道,并基于CSP(Communicating Sequential Processes࿰…...
基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
IT供电系统绝缘监测及故障定位解决方案
随着新能源的快速发展,光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域,IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选,但在长期运行中,例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...
