Merge branch 'jng-QuestionnairePDF-0311' into dev
This commit is contained in:
commit
7867d701de
BIN
data/满意度调成报告模板.pdf
Normal file
BIN
data/满意度调成报告模板.pdf
Normal file
Binary file not shown.
35
pkg/service/bundle/model/questionnaireSurvey.go
Normal file
35
pkg/service/bundle/model/questionnaireSurvey.go
Normal file
@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
type QuestionnairePDFData struct {
|
||||
// 基本信息
|
||||
CustomerNum string `json:"customerNum"`
|
||||
CustomerName string `json:"customerName"`
|
||||
BundleName string `json:"bundleName"`
|
||||
BundleStartDate string `json:"bundleStartDate"`
|
||||
BundleEndDate string `json:"bundleEndDate"`
|
||||
VideoNum string `json:"videoNum"`
|
||||
AccountNum string `json:"accountNum"`
|
||||
ImagesNum string `json:"imagesNum"`
|
||||
DataAnalysisNum string `json:"dataAnalysisNum"`
|
||||
CompetitiveNum string `json:"competitiveNum"`
|
||||
ValueAddVideoNum string `json:"valueAddVideoNum"`
|
||||
|
||||
// 评分(1-5)
|
||||
Score1 int `json:"score1"`
|
||||
Score2 int `json:"score2"`
|
||||
Score3 int `json:"score3"`
|
||||
Score4 int `json:"score4"`
|
||||
Score5 int `json:"score5"`
|
||||
Score6 int `json:"score6"`
|
||||
Score7 int `json:"score7"`
|
||||
|
||||
// 意见
|
||||
Opinion1 string `json:"opinion1"`
|
||||
Opinion2 string `json:"opinion2"`
|
||||
Opinion3 string `json:"opinion3"`
|
||||
|
||||
// 提交信息
|
||||
Submitter string `json:"submitter"`
|
||||
SubmissionDate string `json:"submissionDate"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
@ -2,8 +2,15 @@ package bundle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"fonchain-fiee/api/bundle"
|
||||
"fonchain-fiee/pkg/service"
|
||||
"fonchain-fiee/pkg/service/bundle/model"
|
||||
"fonchain-fiee/pkg/service/upload"
|
||||
"fonchain-fiee/pkg/utils"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@ -56,6 +63,56 @@ func QuestionnaireSurveyCreate(c *gin.Context) {
|
||||
service.Error(c, err)
|
||||
return
|
||||
}
|
||||
address, err := utils.ReverseGeo(req.Longitude, req.Latitude, "ZhCN")
|
||||
if err != nil {
|
||||
service.Error(c, errors.New("获取地址失败"))
|
||||
return
|
||||
}
|
||||
surveyInfo, err := service.BundleProvider.GetQuestionnaireSurveyInfo(c, &bundle.GetQuestionnaireSurveyInfoRequest{UserTel: req.UserTel})
|
||||
if err != nil {
|
||||
service.Error(c, err)
|
||||
return
|
||||
}
|
||||
templateDir := "./data/满意度调成报告模板.pdf"
|
||||
outputPath := "./data/" + req.UserTel + time.Now().Format("20060102150405") + ".pdf"
|
||||
err = utils.QuestionnaireSurveyPDF(templateDir, outputPath, &model.QuestionnairePDFData{
|
||||
//CustomerNum: surveyInfo.BundleInfo.,
|
||||
CustomerName: surveyInfo.UserName,
|
||||
BundleName: surveyInfo.BundleInfo.BundleName,
|
||||
BundleStartDate: surveyInfo.BundleInfo.StartAt,
|
||||
BundleEndDate: surveyInfo.BundleInfo.ExpiredAt,
|
||||
VideoNum: string(surveyInfo.BundleInfo.BundleVideoNumber),
|
||||
AccountNum: string(surveyInfo.BundleInfo.BundleAccountNumber),
|
||||
ImagesNum: string(surveyInfo.BundleInfo.BundleImageNumber),
|
||||
DataAnalysisNum: string(surveyInfo.BundleInfo.BundleDataNumber),
|
||||
CompetitiveNum: string(surveyInfo.BundleInfo.BundleCompetitiveNumber),
|
||||
ValueAddVideoNum: string(surveyInfo.BundleInfo.IncreaseVideoNumber),
|
||||
Score1: int(req.SurveyAnswer.BundleAccountScore),
|
||||
Score2: int(req.SurveyAnswer.BundleAccountScore),
|
||||
Score3: int(req.SurveyAnswer.BundleImageScore),
|
||||
Score4: int(req.SurveyAnswer.BundleDataScore),
|
||||
Score5: int(req.SurveyAnswer.IncreaseVideoScore),
|
||||
Score6: int(req.SurveyAnswer.ServiceResponseSpeed),
|
||||
Score7: int(req.SurveyAnswer.ServiceStaffProfessionalism),
|
||||
Opinion1: req.SurveyFeedback.MeritsReview,
|
||||
Opinion2: req.SurveyFeedback.SuggestionsorImprovements,
|
||||
Opinion3: req.SurveyFeedback.AdditionalComments,
|
||||
Submitter: surveyInfo.UserName,
|
||||
SubmissionDate: time.Now().Format(time.DateOnly),
|
||||
Address: address,
|
||||
})
|
||||
if err != nil {
|
||||
service.Error(c, err)
|
||||
return
|
||||
}
|
||||
os.Remove(outputPath)
|
||||
outputUrl, ossErr := upload.PutBos(outputPath, upload.PdfType, true)
|
||||
if ossErr != nil {
|
||||
service.Error(c, err)
|
||||
return
|
||||
}
|
||||
fmt.Println(outputUrl)
|
||||
service.Success(c, req)
|
||||
res, err := service.BundleProvider.CreateQuestionnaireSurveyAnswer(context.Background(), &req)
|
||||
if err != nil {
|
||||
service.Error(c, err)
|
||||
|
||||
88
pkg/utils/map.go
Normal file
88
pkg/utils/map.go
Normal file
@ -0,0 +1,88 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ReverseGeocodingReq struct {
|
||||
Ak string `json:"ak"`
|
||||
Coordtype string `json:"coordtype"`
|
||||
RetCoordtype string `json:"retCoordtype"`
|
||||
Location string `json:"location"`
|
||||
Output string `json:"output"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
type ReverseGeocodingRes struct {
|
||||
Status int `json:"status"`
|
||||
Result struct {
|
||||
Location struct {
|
||||
Lng float64 `json:"lng"`
|
||||
Lat float64 `json:"lat"`
|
||||
} `json:"location"`
|
||||
FormattedAddress string `json:"formatted_address"`
|
||||
Edz struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"edz"`
|
||||
Business string `json:"business"`
|
||||
AddressComponent struct {
|
||||
Country string `json:"country"`
|
||||
CountryCodeIso string `json:"country_code_iso"`
|
||||
CountryCodeIso2 string `json:"country_code_iso2"`
|
||||
CountryCode int `json:"country_code"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
CityLevel int `json:"city_level"`
|
||||
District string `json:"district"`
|
||||
Town string `json:"town"`
|
||||
TownCode string `json:"town_code"`
|
||||
Distance string `json:"distance"`
|
||||
Direction string `json:"direction"`
|
||||
Adcode string `json:"adcode"`
|
||||
Street string `json:"street"`
|
||||
StreetNumber string `json:"street_number"`
|
||||
} `json:"addressComponent"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
// ReverseGeo 经纬度逆编码
|
||||
func ReverseGeo(longitude, latitude string, language string) (address string, err error) {
|
||||
var reverseGeocodingReq ReverseGeocodingReq
|
||||
|
||||
reverseGeocodingReq.Ak = "3bAjKGA0pv7qvszGe98RsVZ04Ob5r4ZZ"
|
||||
reverseGeocodingReq.Coordtype = "gcj02ll"
|
||||
reverseGeocodingReq.Output = "json"
|
||||
reverseGeocodingReq.RetCoordtype = "gcj02ll"
|
||||
reverseGeocodingReq.Location = strings.Join([]string{latitude, longitude}, ",")
|
||||
reverseGeocodingReq.Language = language
|
||||
|
||||
url := "https://api.map.baidu.com/reverse_geocoding/v3/?ak=" + reverseGeocodingReq.Ak + "&output=" + reverseGeocodingReq.Output + "&coordtype=" + reverseGeocodingReq.Coordtype + "&location=" + reverseGeocodingReq.Location + "&ret_coordtype=" + reverseGeocodingReq.RetCoordtype + "&language=" + reverseGeocodingReq.Language
|
||||
resp, err := http.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var results ReverseGeocodingRes
|
||||
err = json.Unmarshal(body, &results)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if results.Status != 0 {
|
||||
address = "未知地址"
|
||||
return address, err
|
||||
}
|
||||
|
||||
address = results.Result.FormattedAddress
|
||||
|
||||
return address, nil
|
||||
}
|
||||
273
pkg/utils/pdf.go
273
pkg/utils/pdf.go
@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"fonchain-fiee/pkg/service/bundle/model"
|
||||
"image"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -10,7 +11,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/phpdave11/gofpdf"
|
||||
"github.com/signintech/gopdf"
|
||||
@ -595,3 +598,273 @@ func ConvertCompetitorReportToText(data CompetitorReportData, isVideo bool) stri
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
//生成问卷调查pdf
|
||||
|
||||
func QuestionnaireSurveyPDF(templatePath, outputPath string, data *model.QuestionnairePDFData) error {
|
||||
pdf := gopdf.GoPdf{}
|
||||
pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4})
|
||||
|
||||
if err := pdf.ImportPagesFromSource(templatePath, "/MediaBox"); err != nil {
|
||||
return fmt.Errorf("导入模板失败: %w", err)
|
||||
}
|
||||
|
||||
if err := pdf.AddTTFFont("simfang", "./data/simfang.ttf"); err != nil {
|
||||
return fmt.Errorf("加载字体失败: %w", err)
|
||||
}
|
||||
|
||||
if err := pdf.SetFont("simfang", "", 12); err != nil {
|
||||
return fmt.Errorf("设置字体失败: %w", err)
|
||||
}
|
||||
|
||||
startTime, err := time.Parse("2006-01-02", data.BundleStartDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("BundleStartDate格式错误: %w", err)
|
||||
}
|
||||
endTime, err := time.Parse("2006-01-02", data.BundleEndDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("BundleEndDate格式错误: %w", err)
|
||||
}
|
||||
submissionDate, err := time.Parse("2006-01-02", data.SubmissionDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("submissionDate格式错误: %w", err)
|
||||
}
|
||||
nowTime := time.Now().Format(time.DateTime)
|
||||
onePage := 1
|
||||
twoPage := 2
|
||||
threePage := 3
|
||||
fourPage := 4
|
||||
|
||||
// 第1页:客户基本信息
|
||||
pdf.SetPage(onePage)
|
||||
//姓名
|
||||
pdf.SetX(165)
|
||||
pdf.SetY(420)
|
||||
pdf.Cell(nil, data.CustomerName)
|
||||
//套餐名称
|
||||
pdf.SetX(205)
|
||||
pdf.SetY(443)
|
||||
pdf.Cell(nil, data.BundleName)
|
||||
//开始日期
|
||||
pdf.SetX(205)
|
||||
pdf.SetY(467)
|
||||
pdf.Cell(nil, startTime.Format("2006"))
|
||||
pdf.SetX(260)
|
||||
pdf.SetY(467)
|
||||
pdf.Cell(nil, startTime.Format("01"))
|
||||
pdf.SetX(300)
|
||||
pdf.SetY(467)
|
||||
pdf.Cell(nil, startTime.Format("02"))
|
||||
|
||||
//结束日期
|
||||
pdf.SetX(350)
|
||||
pdf.SetY(467)
|
||||
pdf.Cell(nil, endTime.Format("2006"))
|
||||
pdf.SetX(398)
|
||||
pdf.SetY(467)
|
||||
pdf.Cell(nil, endTime.Format("01"))
|
||||
pdf.SetX(437)
|
||||
pdf.SetY(467)
|
||||
pdf.Cell(nil, endTime.Format("02"))
|
||||
//视频数
|
||||
pdf.SetX(220)
|
||||
pdf.SetY(583)
|
||||
pdf.Cell(nil, data.VideoNum)
|
||||
//"账号数: "+
|
||||
pdf.SetX(230)
|
||||
pdf.SetY(625)
|
||||
pdf.Cell(nil, data.AccountNum)
|
||||
// "图文数: "+
|
||||
pdf.SetX(253)
|
||||
pdf.SetY(667)
|
||||
pdf.Cell(nil, data.ImagesNum)
|
||||
//"数据分析数: "+
|
||||
pdf.SetX(280)
|
||||
pdf.SetY(727)
|
||||
pdf.Cell(nil, data.DataAnalysisNum)
|
||||
// 第1页内容写完后
|
||||
if err = addWatermark(&pdf, "确认地址:"+data.Address+"\n确认时间:"+nowTime); err != nil {
|
||||
return err
|
||||
}
|
||||
// 第2页:服务数量
|
||||
pdf.SetPage(twoPage)
|
||||
|
||||
//"竞品分析数: "+
|
||||
pdf.SetX(205)
|
||||
pdf.SetY(72)
|
||||
pdf.Cell(nil, data.CompetitiveNum)
|
||||
//"增值视频数: "+
|
||||
pdf.SetX(270)
|
||||
pdf.SetY(156)
|
||||
pdf.Cell(nil, data.ValueAddVideoNum)
|
||||
//"评分1: "+
|
||||
pdf.SetX(123)
|
||||
pdf.SetY(485)
|
||||
pdf.Cell(nil, scoreStars(data.Score1))
|
||||
//"评分2: "+
|
||||
pdf.SetX(343)
|
||||
pdf.SetY(526)
|
||||
pdf.Cell(nil, scoreStars(data.Score2))
|
||||
//"评分3: "+
|
||||
pdf.SetX(230)
|
||||
pdf.SetY(568)
|
||||
pdf.Cell(nil, scoreStars(data.Score3))
|
||||
//"评分4: "+
|
||||
pdf.SetX(362)
|
||||
pdf.SetY(610)
|
||||
pdf.Cell(nil, scoreStars(data.Score4))
|
||||
//"评分5: "+
|
||||
pdf.SetX(220)
|
||||
pdf.SetY(652)
|
||||
pdf.Cell(nil, scoreStars(data.Score5))
|
||||
//"评分6: "+
|
||||
pdf.SetX(164)
|
||||
pdf.SetY(694)
|
||||
pdf.Cell(nil, scoreStars(data.Score6))
|
||||
//"评分7: "+
|
||||
pdf.SetX(197)
|
||||
pdf.SetY(735)
|
||||
pdf.Cell(nil, scoreStars(data.Score7))
|
||||
//水印
|
||||
if err = addWatermark(&pdf, "确认地址:"+data.Address+"\n确认时间:"+nowTime); err != nil {
|
||||
return err
|
||||
}
|
||||
// 第3页:评分与意见
|
||||
pdf.SetPage(threePage)
|
||||
// Opinion 超过100字符时自动换行,每行约20个中文字符,行高18pt
|
||||
drawWrappedText(&pdf, data.Opinion1, 90, 145, 24, 34)
|
||||
drawWrappedText(&pdf, data.Opinion2, 90, 377, 24, 34)
|
||||
drawWrappedText(&pdf, data.Opinion3, 90, 574, 24, 34)
|
||||
//水印
|
||||
if err = addWatermark(&pdf, "确认地址:"+data.Address+"\n确认时间:"+nowTime); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 第4页:提交信息
|
||||
pdf.SetPage(fourPage)
|
||||
//"提交人: "+
|
||||
pdf.SetX(135)
|
||||
pdf.SetY(103)
|
||||
pdf.Cell(nil, data.Submitter)
|
||||
//提交时间: "
|
||||
pdf.SetX(148)
|
||||
pdf.SetY(128)
|
||||
pdf.Cell(nil, submissionDate.Format("2006"))
|
||||
pdf.SetX(207)
|
||||
pdf.SetY(128)
|
||||
pdf.Cell(nil, submissionDate.Format("01"))
|
||||
pdf.SetX(260)
|
||||
pdf.SetY(128)
|
||||
pdf.Cell(nil, submissionDate.Format("02"))
|
||||
//水印
|
||||
if err = addWatermark(&pdf, "确认地址:"+data.Address+"\n确认时间:"+nowTime); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pdf.WritePdf(outputPath); err != nil {
|
||||
return fmt.Errorf("写入PDF失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addWatermark 在当前页叠加浅色斜向双行水印,并在结束后恢复样式
|
||||
func addWatermark(pdf *gopdf.GoPdf, text string) error {
|
||||
const normalFontSize = 12
|
||||
const watermarkFontSize = 22
|
||||
const lineHeight = 32.0 // 水印行间距
|
||||
|
||||
// 设置水印样式
|
||||
pdf.SetGrayFill(0.85)
|
||||
if err := pdf.SetFont("simfang", "", watermarkFontSize); err != nil {
|
||||
return fmt.Errorf("设置水印字体失败: %w", err)
|
||||
}
|
||||
|
||||
// 按换行拆分,逐行绘制(Cell 不负责自动换行)
|
||||
lines := strings.Split(text, "\n")
|
||||
if len(lines) == 0 {
|
||||
lines = []string{text}
|
||||
}
|
||||
|
||||
drawBlock := func(x, y, cx, cy float64) {
|
||||
pdf.Rotate(35, cx, cy)
|
||||
for i, line := range lines {
|
||||
pdf.SetX(x)
|
||||
pdf.SetY(y + float64(i)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
}
|
||||
pdf.RotateReset()
|
||||
}
|
||||
|
||||
// 两处重复水印
|
||||
drawBlock(90, 420, 300, 420)
|
||||
drawBlock(130, 620, 300, 420)
|
||||
|
||||
// 恢复样式,避免影响后续正文
|
||||
pdf.SetGrayFill(0)
|
||||
if err := pdf.SetFont("simfang", "", normalFontSize); err != nil {
|
||||
return fmt.Errorf("恢复字体失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawWrappedText 在 PDF 上绘制自动换行的文字
|
||||
// pdf: GoPdf 实例, text: 文字内容, startX/startY: 起始坐标
|
||||
// maxWidth: 最大宽度(pt), lineHeight: 行高(pt), charsPerLine: 每行最多字符数(按中文字符计)
|
||||
func drawWrappedText(pdf *gopdf.GoPdf, text string, startX, startY, lineHeight float64, charsPerLine int) {
|
||||
runes := []rune(text)
|
||||
total := len(runes)
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
lineStart := 0
|
||||
currentLine := 0
|
||||
|
||||
for lineStart < total {
|
||||
end := lineStart + charsPerLine
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
// 计算实际宽度以决定换行点(按字节估算:ASCII=0.5中文字符宽)
|
||||
charCount := 0
|
||||
splitAt := lineStart
|
||||
for i := lineStart; i < total; i++ {
|
||||
r := runes[i]
|
||||
runeBytes := utf8.RuneLen(r)
|
||||
if runeBytes > 1 {
|
||||
charCount += 2 // 中文等宽字符算2个单位
|
||||
} else {
|
||||
charCount += 1 // ASCII 算1个单位
|
||||
}
|
||||
if charCount > charsPerLine*2 {
|
||||
break
|
||||
}
|
||||
splitAt = i + 1
|
||||
}
|
||||
if splitAt == lineStart {
|
||||
splitAt = lineStart + 1
|
||||
}
|
||||
|
||||
line := string(runes[lineStart:splitAt])
|
||||
pdf.SetX(startX)
|
||||
pdf.SetY(startY + float64(currentLine)*lineHeight)
|
||||
pdf.Cell(nil, line)
|
||||
|
||||
lineStart = splitAt
|
||||
currentLine++
|
||||
}
|
||||
}
|
||||
|
||||
func scoreStars(score int) string {
|
||||
switch {
|
||||
case score <= 1:
|
||||
return "★☆☆☆☆"
|
||||
case score == 2:
|
||||
return "★★☆☆☆"
|
||||
case score == 3:
|
||||
return "★★★☆☆"
|
||||
case score == 4:
|
||||
return "★★★★☆"
|
||||
default:
|
||||
return "★★★★★"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user