Beego问卷统计核心是数据映射与安全聚合:GROUP BY字段须显式出现在SELECT中;用Raw+参数化防注入;QueryRows需字段名严格匹配别名;跨表统计禁用ORM链式调用;导出Excel需手动转指针切片为[]interface{};模糊搜索应在Go层加%并使用参数绑定。

Beego 实现问卷调查统计分析,核心不在 ORM 增删改查本身,而在于「如何把原始提交数据映射为可聚合的结构」+「避免 SQL 注入与类型错位」。
用 Raw 执行聚合查询时,GROUP BY 字段必须显式出现在 SELECT 列表中
MySQL 5.7+ 默认开启 sql_mode=only_full_group_by,直接写 SELECT COUNT(*) FROM answers GROUP BY question_id 会报错:Expression #1 of SELECT list is not in GROUP BY clause。
常见错误写法:
sql := `SELECT COUNT(*) FROM answers WHERE survey_id = ? GROUP BY question_id` db.Raw(sql, surveyID).QueryRows(&result)
正确做法是明确写出分组字段:
- 使用
SELECT question_id, COUNT(*) AS count,确保question_id在SELECT中 - 若需关联题目文本,用
JOIN而非子查询(子查询在Raw中易漏参数绑定) - Beego 的
QueryRows不自动映射字段别名,结构体字段名必须与 SQL 中列名(或别名)严格一致,比如Count对应COUNT(*)就不行,得写COUNT(*) AS count并配count int字段
Filter + RelatedSel 不适用于跨表统计,该用 Raw 就别硬套 ORM 链式调用
有人试图这样查某题各选项被选次数:
o.QueryTable("answer_options").
Filter("answer__survey_id", surveyID).
Filter("answer__question_id", qid).
GroupBy("option_id").
Count()
这会失败——Beego ORM 的 GroupBy 仅支持单表,且不生成 GROUP BY 子句,实际执行的是全量拉取后 Go 层聚合,内存和性能双崩。
真正可行的路径:
- 写原生 SQL,用
Raw+ 参数化占位符(?)防注入 - 聚合结果结构体字段名必须小写(Beego 默认 JSON 输出小写),如
OptionID int `json:"option_id"` - 若统计维度多(如按日期+题目+选项),SQL 拼接建议用
strings.Builder,别用+=多次字符串拼接
导出 Excel 时,QueryRows 返回的切片不能直接给 excelize 写入
Beego QueryRows 返回的是指针切片(*[]T),而 excelize.SetSheetRow 需要值切片([]interface{})。
典型翻车现场:
var rows []*models.AnswerStat
db.Raw(sql).QueryRows(&rows) // rows 是 *[]AnswerStat 类型
f.SetSheetRow("Sheet1", "A1", &rows) // panic: unmarshal type error
解决办法只有手动转换:
- 遍历
rows,对每个结构体字段提取值,构造成[]interface{} - 别偷懒用反射自动转——字段顺序错一位,Excel 表头就全乱
- 时间字段注意用
time.Time.Format("2006-01-02")转字符串再塞进去,excelize不认time.Time
前端传来的 keyword 搜索,用 LIKE 必须手动加 %,且不能直接拼进 SQL
看知识库里的 GetDataClassListData 函数,它写的是:
if keyword != "" {
sql += " AND table_name = ?"
params = append(params, keyword)
}
这是精确匹配。但问卷统计常需模糊搜题干,比如 keyword="满意度",应匹配含“满意度”的所有题目。此时必须:
- 把
%加在 Go 层:params = append(params, "%"+keyword+"%") - SQL 改成
AND question_text LIKE ?,绝不能写成"%" + keyword + "%"拼进 SQL 字符串(SQL 注入!) - 注意 MySQL 的
LIKE区分大小写取决于字段 collation,测试环境用utf8mb4_general_ci,生产可能用utf8mb4_0900_as_cs,大小写敏感时搜不到就懵
最易被忽略的点:统计接口的 currentpage 和 pagesize 是从 0 开始还是 1 开始?知识库示例里 fmt.Sprintf(" LIMIT %d,%d", currentpage, pagesize) 是 MySQL 的 LIMIT offset, limit 语法,offset 是跳过的行数,不是页码。传 page=1 时得算成 (page-1)*pagesize,否则第一页永远空。这个换算逻辑,必须在 controller 层做,不能甩给前端或 SQL。


















