h_exam.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. package handler
  2. import (
  3. webServerEntity "cicv-data-closedloop/amd64/web_server/entity"
  4. "cicv-data-closedloop/common/config/c_db"
  5. "cicv-data-closedloop/common/config/c_log"
  6. commonEntity "cicv-data-closedloop/common/entity"
  7. "cicv-data-closedloop/common/util"
  8. "github.com/gin-gonic/gin"
  9. "github.com/signintech/gopdf"
  10. "io"
  11. "log"
  12. "math"
  13. "net/http"
  14. "os"
  15. "sync"
  16. "time"
  17. )
  18. var (
  19. defaultTime = time.Date(2006, time.January, 2, 15, 4, 5, 0, time.Local)
  20. cacheMutex sync.Mutex
  21. cacheTeamName = make(map[string]time.Time)
  22. heartBeatTimeThreshold = 5 * time.Second // 心跳时间
  23. InitialPositionX = 0.00 // todo 需要比赛确认起点
  24. InitialPositionY = 0.00 // todo 需要比赛确认起点
  25. // todo 比赛阶段
  26. trialBegin = time.Date(2024, time.June, 16, 13, 00, 00, 0, time.Local)
  27. trialEnd = time.Date(2024, time.June, 19, 23, 59, 59, 0, time.Local)
  28. )
  29. // 考试心跳
  30. func Tick(c *gin.Context) {
  31. param := new(webServerEntity.ExamPao)
  32. // 映射到结构体
  33. if err := c.ShouldBindJSON(&param); err != nil {
  34. c_log.GlobalLogger.Error("请求体解析失败,错误信息为:", err)
  35. c.JSON(http.StatusBadRequest, commonEntity.Response{
  36. Code: 500,
  37. Msg: "请求体解析失败。",
  38. })
  39. return
  40. }
  41. teamName := param.TeamName
  42. positionX := param.PositionX
  43. positionY := param.PositionY
  44. if !util.ContainsKey(cacheTeamName, teamName) && math.Abs(positionX-InitialPositionX) < 5.00 && math.Abs(positionY-InitialPositionY) < 5.00 { // (在起点开始)
  45. sqlTemplate, _ := util.ReadFile(c_db.SqlFilesMap["exam-insert-begin_time-and-topic-by-team_name.sql"])
  46. c_log.GlobalLogger.Info("保存比赛开始时间和比赛阶段", sqlTemplate)
  47. stage := "表演赛"
  48. if time.Now().After(trialBegin) && time.Now().Before(trialEnd) {
  49. stage = "预赛"
  50. } else {
  51. stage = "决赛"
  52. }
  53. if err := c_db.DoTx(sqlTemplate, []any{
  54. param.TeamName,
  55. stage,
  56. time.Now(),
  57. }); err != nil {
  58. c_log.GlobalLogger.Error("保存比赛开始时间报错:", err)
  59. c.JSON(http.StatusBadRequest, commonEntity.Response{
  60. Code: 500,
  61. Msg: "保存比赛开始时间报错。",
  62. })
  63. return
  64. }
  65. } else if !util.ContainsKey(cacheTeamName, teamName) && math.Abs(positionX-InitialPositionX) < 5.00 && math.Abs(positionY-InitialPositionY) < 5.00 { // 不在起点(开始)
  66. // 车辆不是在起点启动的自动驾驶模式
  67. selectSql, err := util.ReadFile(c_db.SqlFilesMap["exam-select-latest-by-team_name.sql"])
  68. if err != nil {
  69. c_log.GlobalLogger.Error("读取sql文件报错:", err)
  70. return
  71. }
  72. // 可以传参数
  73. var result []webServerEntity.ExamPo
  74. if err = c_db.MysqlDb.Select(&result, selectSql, teamName); err != nil {
  75. c_log.GlobalLogger.Error("数据库查询报错:", err)
  76. return
  77. }
  78. if len(result) == 1 {
  79. c_log.GlobalLogger.Info("上一条数据记录为:", result[0])
  80. }
  81. // 添加队伍名到缓存中
  82. cacheTeamName[teamName] = time.Now()
  83. // 更新记录结束时间为默认时间
  84. sqlTemplate, _ := util.ReadFile(c_db.SqlFilesMap["exam-update-end_time-by-team_name.sql"])
  85. if err = c_db.DoTx(sqlTemplate, []any{
  86. defaultTime,
  87. teamName,
  88. }); err != nil {
  89. c_log.GlobalLogger.Error("插入数据报错:", err)
  90. return
  91. }
  92. c_log.GlobalLogger.Infof("队伍 %v 的考试在中途中断后重新开始。", teamName)
  93. } else if util.ContainsKey(cacheTeamName, teamName) { // 进行中
  94. cacheTeamName[teamName] = time.Now()
  95. }
  96. c.JSON(http.StatusOK, commonEntity.Response{
  97. Code: 200,
  98. Msg: "心跳接收成功。",
  99. })
  100. }
  101. func ExamEndTicker() {
  102. // 创建一个定时器,每隔一秒触发一次
  103. ticker := time.NewTicker(1 * time.Second)
  104. for {
  105. select {
  106. // 定时器触发时执行的代码
  107. case <-ticker.C:
  108. cacheMutex.Lock()
  109. {
  110. var keysToDelete []string
  111. for teamName, heartBeatTime := range cacheTeamName {
  112. if time.Since(heartBeatTime) > heartBeatTimeThreshold { // 检查缓存中的队名,如果超过心跳时间,则代表考试结束,删除缓存中的队名
  113. keysToDelete = append(keysToDelete, teamName)
  114. }
  115. }
  116. for _, teamName := range keysToDelete { // 检查缓存中的队名,如果超过心跳时间,则代表考试结束,删除缓存中的队名
  117. delete(cacheTeamName, teamName)
  118. // 1 查询指定队伍的开始时间最新的考试是否有结束时间,如果有则不在处理,如果没有则更新
  119. var result []webServerEntity.ExamPo
  120. selectSql, err := util.ReadFile(c_db.SqlFilesMap["exam-select-latest-by-team_name.sql"])
  121. if err != nil {
  122. c_log.GlobalLogger.Error("读取sql文件报错:", err)
  123. return
  124. }
  125. // 可以传参数
  126. if err = c_db.MysqlDb.Select(&result, selectSql, teamName); err != nil {
  127. c_log.GlobalLogger.Error("数据库查询报错:", err)
  128. return
  129. }
  130. c_log.GlobalLogger.Info("数据库查询成功:", result)
  131. if !result[0].EndTime.Equal(defaultTime) {
  132. c_log.GlobalLogger.Error("赛队", teamName, "考试已结束!")
  133. return
  134. }
  135. // 更新到数据库
  136. sqlTemplate, _ := util.ReadFile(c_db.SqlFilesMap["exam-update-end_time-by-team_name.sql"])
  137. if err := c_db.DoTx(sqlTemplate, []any{
  138. time.Now(),
  139. teamName,
  140. }); err != nil {
  141. c_log.GlobalLogger.Error("插入数据报错:", err)
  142. return
  143. }
  144. c_log.GlobalLogger.Infof("队伍 %v 的考试结束。", teamName)
  145. }
  146. }
  147. cacheMutex.Unlock()
  148. }
  149. }
  150. }
  151. // 考试开始时间
  152. func Begin(c *gin.Context) {
  153. param := new(webServerEntity.ExamPao)
  154. // 映射到结构体
  155. if err := c.ShouldBindJSON(&param); err != nil {
  156. c_log.GlobalLogger.Error("项目启动接收请求参数报错:", err)
  157. c.JSON(http.StatusBadRequest, commonEntity.Response{
  158. Code: 500,
  159. Msg: "请求体解析失败。",
  160. })
  161. return
  162. }
  163. // 插入到数据库
  164. sqlTemplate, _ := util.ReadFile(c_db.SqlFilesMap["exam-insert-begin_time-by-team_name.sql"])
  165. c_log.GlobalLogger.Info("插入比赛开始时间", sqlTemplate)
  166. if err := c_db.DoTx(sqlTemplate, []any{
  167. param.TeamName,
  168. time.Now(),
  169. }); err != nil {
  170. c_log.GlobalLogger.Error("插入数据报错:", err)
  171. c.JSON(http.StatusBadRequest, commonEntity.Response{
  172. Code: 500,
  173. Msg: "插入数据报错。",
  174. })
  175. return
  176. }
  177. c.JSON(http.StatusOK, commonEntity.Response{
  178. Code: 200,
  179. Msg: "插入数据成功。",
  180. })
  181. }
  182. // 考试结束时间
  183. func End(c *gin.Context) {
  184. param := new(webServerEntity.ExamPao)
  185. // 映射到结构体
  186. if err := c.ShouldBindJSON(&param); err != nil {
  187. c_log.GlobalLogger.Error("项目启动接收请求参数报错:", err)
  188. c.JSON(http.StatusBadRequest, commonEntity.Response{
  189. Code: 500,
  190. Msg: "请求体解析失败。",
  191. })
  192. return
  193. }
  194. // 1 查询指定队伍的开始时间最新的考试是否有结束时间,如果有则不在处理,如果没有则更新
  195. var result []webServerEntity.ExamPo
  196. selectSql, err := util.ReadFile(c_db.SqlFilesMap["exam-select-latest-by-team_name.sql"])
  197. if err != nil {
  198. c_log.GlobalLogger.Error("读取sql文件报错:", err)
  199. c.JSON(http.StatusBadRequest, commonEntity.Response{
  200. Code: 500,
  201. Msg: "读取sql文件报错。",
  202. })
  203. return
  204. }
  205. // 可以传参数
  206. if err = c_db.MysqlDb.Select(&result, selectSql, param.TeamName); err != nil {
  207. c_log.GlobalLogger.Error("数据库查询报错:", err)
  208. c.JSON(http.StatusBadRequest, commonEntity.Response{
  209. Code: 500,
  210. Msg: "数据库查询报错。",
  211. })
  212. return
  213. }
  214. c_log.GlobalLogger.Info("数据库查询成功:", result)
  215. if !result[0].EndTime.Equal(defaultTime) {
  216. c_log.GlobalLogger.Error("赛队", param.TeamName, "重复请求考试结束接口!")
  217. c.JSON(http.StatusBadRequest, commonEntity.Response{
  218. Code: 500,
  219. Msg: "重复请求。",
  220. })
  221. return
  222. }
  223. // 更新到数据库
  224. sqlTemplate, _ := util.ReadFile(c_db.SqlFilesMap["exam-update-end_time-by-team_name.sql"])
  225. if err := c_db.DoTx(sqlTemplate, []any{
  226. time.Now(),
  227. param.TeamName,
  228. }); err != nil {
  229. c_log.GlobalLogger.Error("插入数据报错:", err)
  230. c.JSON(http.StatusBadRequest, commonEntity.Response{
  231. Code: 500,
  232. Msg: "插入数据报错。",
  233. })
  234. return
  235. }
  236. c.JSON(http.StatusOK, commonEntity.Response{
  237. Code: 200,
  238. Msg: "插入数据成功。",
  239. })
  240. }
  241. // 分页查询
  242. // todo 如果日期为默认值,则返回空""
  243. func Page(c *gin.Context) {
  244. param := new(webServerEntity.ExamPagePao)
  245. _ = c.ShouldBindJSON(&param)
  246. var resultPos []webServerEntity.ExamPo
  247. var resultPosTotal []int
  248. var pageSql string
  249. var totalSql string
  250. offset := (param.CurrentPage - 1) * param.PageSize
  251. size := param.PageSize
  252. if param.TeamName == "" && param.Topic == "" {
  253. pageSql, _ = util.ReadFile(c_db.SqlFilesMap["exam-select-page.sql"])
  254. totalSql, _ = util.ReadFile(c_db.SqlFilesMap["exam-select-total.sql"])
  255. err := c_db.MysqlDb.Select(&resultPos, pageSql, offset, size)
  256. if err != nil {
  257. c_log.GlobalLogger.Error(err)
  258. }
  259. err = c_db.MysqlDb.Select(&resultPosTotal, totalSql)
  260. if err != nil {
  261. c_log.GlobalLogger.Error(err)
  262. }
  263. }
  264. if param.TeamName != "" && param.Topic == "" {
  265. pageSql, _ = util.ReadFile(c_db.SqlFilesMap["exam-select-page-by-team_name.sql"])
  266. totalSql, _ = util.ReadFile(c_db.SqlFilesMap["exam-select-total-by-team_name.sql"])
  267. _ = c_db.MysqlDb.Select(&resultPos, pageSql, "%"+param.TeamName+"%", offset, size)
  268. _ = c_db.MysqlDb.Select(&resultPosTotal, totalSql, "%"+param.TeamName+"%")
  269. }
  270. if param.TeamName == "" && param.Topic != "" {
  271. pageSql, _ = util.ReadFile(c_db.SqlFilesMap["exam-select-page-by-topic.sql"])
  272. totalSql, _ = util.ReadFile(c_db.SqlFilesMap["exam-select-total-by-topic.sql"])
  273. _ = c_db.MysqlDb.Select(&resultPos, pageSql, "%"+param.Topic+"%", offset, size)
  274. _ = c_db.MysqlDb.Select(&resultPosTotal, totalSql, "%"+param.Topic+"%")
  275. }
  276. if param.TeamName != "" && param.Topic != "" {
  277. pageSql, _ = util.ReadFile(c_db.SqlFilesMap["exam-select-page-by-team_name-and-topic.sql"])
  278. totalSql, _ = util.ReadFile(c_db.SqlFilesMap["exam-select-total-by-team_name-and-topic.sql"])
  279. _ = c_db.MysqlDb.Select(&resultPos, pageSql, "%"+param.TeamName+"%", "%"+param.Topic+"%", offset, size)
  280. _ = c_db.MysqlDb.Select(&resultPosTotal, totalSql, "%"+param.TeamName+"%", "%"+param.Topic+"%")
  281. }
  282. var resultVos []webServerEntity.ExamVo
  283. for _, po := range resultPos {
  284. resultVos = append(resultVos, webServerEntity.ExamVo{
  285. Id: po.Id,
  286. TeamName: po.TeamName,
  287. Topic: po.Topic,
  288. BeginTime: util.GetTimeString(po.BeginTime),
  289. EndTime: util.GetTimeString(po.EndTime),
  290. ScoreOnline: po.ScoreOnline,
  291. ScoreOffline: po.ScoreOffline,
  292. ScoreFinal: po.ScoreFinal,
  293. Details: po.Details,
  294. ScoreReportPath: po.ScoreReportPath,
  295. })
  296. }
  297. c.JSON(http.StatusOK, commonEntity.Response{
  298. Code: 200,
  299. Msg: "分页查询成功!",
  300. Data: resultVos,
  301. Total: resultPosTotal[0],
  302. })
  303. }
  304. // 评分报告pdf下载
  305. func Report(c *gin.Context) {
  306. //param := new(webServerEntity.ExamReportPao)
  307. //// 映射到结构体
  308. //if err := c.ShouldBindJSON(&param); err != nil {
  309. // c_log.GlobalLogger.Error("项目启动接收请求参数报错:", err)
  310. // c.JSON(http.StatusBadRequest, commonEntity.Response{
  311. // Code: 500,
  312. // Msg: "请求体解析失败。",
  313. // })
  314. // return
  315. //}
  316. // 1 根据ID查询数据
  317. // 2 根据数据生成pdf
  318. // 3 创建pdf文件
  319. {
  320. // 初始化 pdf 对象
  321. pdf := gopdf.GoPdf{}
  322. pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4})
  323. // 添加字体文件到pdf
  324. err := pdf.AddTTFFont("simfang", "D:\\code\\cicv-data-closedloop\\amd64\\web_server\\simfang.ttf")
  325. if err != nil {
  326. log.Print(err.Error())
  327. return
  328. }
  329. leftMargin := 70.0
  330. topMargin := 100.0
  331. lineHeight := 25.0
  332. tableWidth := 450.0
  333. paragraphSpacing := 10.0
  334. pdf.SetLeftMargin(leftMargin) // 设置左边距
  335. //alignOptionText := gopdf.CellOption{Align: gopdf.Left | gopdf.Middle}
  336. //alignOptionText2 := gopdf.CellOption{Align: gopdf.Center | gopdf.Middle}
  337. alignOptionTable := gopdf.CellOption{Align: gopdf.Center | gopdf.Middle, Border: gopdf.Left | gopdf.Right | gopdf.Bottom | gopdf.Top}
  338. // ------- 封面页 -------
  339. pdf.AddPage()
  340. {
  341. err = pdf.SetFont("simfang", "", 36)
  342. err = pdf.Image("D:\\code\\cicv-data-closedloop\\amd64\\web_server\\background.png", 0, 0, nil) // 背景图片
  343. err = pdf.Image("D:\\code\\cicv-data-closedloop\\amd64\\web_server\\logo.png", 20, 20, &gopdf.Rect{W: 320, H: 100})
  344. {
  345. pdf.SetXY(100, 250)
  346. _ = pdf.Text("车路云一体化算法挑战赛")
  347. }
  348. {
  349. pdf.SetXY(220, 315) // 200
  350. _ = pdf.Text("评分报告")
  351. }
  352. _ = pdf.SetFont("simfang", "", 14)
  353. {
  354. pdf.SetXY(200, 525)
  355. _ = pdf.Text("赛队编号:01")
  356. }
  357. {
  358. pdf.SetXY(200, 545)
  359. _ = pdf.Cell(nil, "评测车型:多功能车平台")
  360. }
  361. {
  362. pdf.SetXY(200, 565)
  363. _ = pdf.Cell(nil, "评测地点:天津生态城")
  364. }
  365. {
  366. pdf.SetXY(200, 585)
  367. _ = pdf.Cell(nil, "报告时间:2023年10月12日")
  368. }
  369. {
  370. pdf.SetXY(165, 775)
  371. _ = pdf.Cell(nil, "国汽(北京)智能网联汽车研究院有限公司")
  372. }
  373. }
  374. // todo 先不要目录页
  375. //// ------- 目录页 -------
  376. //{
  377. // pdf.AddPage()
  378. // currentLine := 0.0
  379. // {
  380. // err = pdf.SetFont("simfang", "", 18)
  381. // pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  382. // _ = pdf.CellWithOption(&gopdf.Rect{W: tableWidth, H: lineHeight}, "目录", alignOptionText2)
  383. // currentLine++
  384. // currentLine++
  385. // }
  386. // {
  387. // err = pdf.SetFont("simfang", "", 14)
  388. // pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  389. // currentLine++
  390. // _ = pdf.CellWithOption(&gopdf.Rect{W: tableWidth, H: lineHeight}, "1. 总体情况 ............................... 1", alignOptionText)
  391. // }
  392. //}
  393. // ------- 详情页 ------- todo 详情页暂时不分页,因为内容可能很长
  394. {
  395. pdf.AddPage()
  396. currentLine := 0.0
  397. // 1. 总体分数情况
  398. {
  399. {
  400. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  401. currentLine++
  402. err = pdf.Text("1. 总体分数情况")
  403. }
  404. {
  405. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  406. currentLine++
  407. _ = pdf.Text("整体得分:")
  408. }
  409. {
  410. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  411. currentLine++
  412. _ = pdf.Text("线上得分:")
  413. }
  414. {
  415. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  416. currentLine++
  417. _ = pdf.Text("线下得分:")
  418. }
  419. {
  420. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  421. currentLine++
  422. _ = pdf.Text("整体耗时:")
  423. }
  424. }
  425. // 2. 分数详情
  426. {
  427. columnNumber := 4.0 // 列数
  428. tableCellWidth := tableWidth / columnNumber // 单元格宽度
  429. {
  430. pdf.SetXY(leftMargin, paragraphSpacing+topMargin+currentLine*lineHeight)
  431. currentLine++
  432. _ = pdf.Cell(nil, "2. 分数详情")
  433. }
  434. // 分数详情表格
  435. {
  436. {
  437. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  438. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "序号", alignOptionTable)
  439. }
  440. {
  441. pdf.SetXY(leftMargin+1.0*tableCellWidth, topMargin+currentLine*lineHeight)
  442. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "场景名称", alignOptionTable)
  443. }
  444. {
  445. pdf.SetXY(leftMargin+2.0*tableCellWidth, topMargin+currentLine*lineHeight)
  446. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "线上得分", alignOptionTable)
  447. }
  448. {
  449. pdf.SetXY(leftMargin+3.0*tableCellWidth, topMargin+currentLine*lineHeight)
  450. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "线下得分", alignOptionTable)
  451. }
  452. currentLine++
  453. }
  454. for i := 0; i < 10; i++ {
  455. {
  456. {
  457. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  458. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, util.ToString(i+1), alignOptionTable)
  459. }
  460. {
  461. pdf.SetXY(leftMargin+1.0*tableCellWidth, topMargin+currentLine*lineHeight)
  462. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "xxx", alignOptionTable)
  463. }
  464. {
  465. pdf.SetXY(leftMargin+2.0*tableCellWidth, topMargin+currentLine*lineHeight)
  466. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "xxx", alignOptionTable)
  467. }
  468. {
  469. pdf.SetXY(leftMargin+3.0*tableCellWidth, topMargin+currentLine*lineHeight)
  470. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "xxx", alignOptionTable)
  471. }
  472. currentLine++
  473. }
  474. }
  475. }
  476. // 3. 线上扣分详情
  477. {
  478. columnNumber := 3.0 // 列数
  479. tableCellWidth := tableWidth / columnNumber // 单元格宽度
  480. {
  481. pdf.SetXY(leftMargin, paragraphSpacing+topMargin+currentLine*lineHeight)
  482. currentLine++
  483. err = pdf.Cell(nil, "3. 线上扣分详情")
  484. }
  485. for i := 0; i < 10; i++ {
  486. // 场景1
  487. {
  488. {
  489. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  490. currentLine++
  491. err = pdf.Cell(nil, "("+util.ToString(i+1)+")xxxx场景")
  492. }
  493. // 场景1 表格
  494. {
  495. {
  496. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  497. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "序号", alignOptionTable)
  498. }
  499. {
  500. pdf.SetXY(leftMargin+1.0*tableCellWidth, topMargin+currentLine*lineHeight)
  501. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "扣分点", alignOptionTable)
  502. }
  503. {
  504. pdf.SetXY(leftMargin+2.0*tableCellWidth, topMargin+currentLine*lineHeight)
  505. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "扣分说明", alignOptionTable)
  506. }
  507. currentLine++
  508. }
  509. for j := 0; j < 4; j++ {
  510. {
  511. {
  512. pdf.SetXY(leftMargin, topMargin+currentLine*lineHeight)
  513. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, util.ToString(j+1), alignOptionTable)
  514. }
  515. {
  516. pdf.SetXY(leftMargin+1.0*tableCellWidth, topMargin+currentLine*lineHeight)
  517. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "xxx", alignOptionTable)
  518. }
  519. {
  520. pdf.SetXY(leftMargin+2.0*tableCellWidth, topMargin+currentLine*lineHeight)
  521. _ = pdf.CellWithOption(&gopdf.Rect{W: tableCellWidth, H: lineHeight}, "xxx", alignOptionTable)
  522. }
  523. currentLine++
  524. }
  525. }
  526. }
  527. }
  528. }
  529. // 4 作品优化建议
  530. {
  531. pdf.SetXY(leftMargin, paragraphSpacing+topMargin+currentLine*lineHeight)
  532. currentLine++
  533. err = pdf.Cell(nil, "4. 作品优化建议")
  534. }
  535. }
  536. // ------- 附录页 -------
  537. pdf.AddPage()
  538. {
  539. pdf.SetXY(leftMargin, topMargin)
  540. _ = pdf.Text("附件:评分规则")
  541. pdf.AddExternalLink("https://www.baidu.com/", leftMargin, topMargin-lineHeight, 6*14, lineHeight)
  542. }
  543. // 写入本地
  544. pdf.WritePdf("D:\\hello.pdf")
  545. }
  546. // 打开要发送的文件
  547. file, err := os.Open("D:\\hello.pdf")
  548. if err != nil {
  549. c.String(http.StatusNotFound, "文件未找到")
  550. return
  551. }
  552. defer file.Close()
  553. // 将文件复制到响应主体
  554. _, err = io.Copy(c.Writer, file)
  555. if err != nil {
  556. c.String(http.StatusInternalServerError, "无法复制文件到响应体")
  557. return
  558. }
  559. }