Files
bd-server/app/main/api/internal/service/authorizationService.go

461 lines
15 KiB
Go
Raw Normal View History

2026-05-08 11:30:05 +08:00
package service
import (
"bd-server/app/main/api/internal/config"
"bd-server/app/main/model"
"bytes"
"context"
"database/sql"
"encoding/base64"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/jung-kurt/gofpdf"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
)
type AuthorizationService struct {
config config.Config
authDocModel model.AuthorizationDocumentModel
fileStoragePath string
fileBaseURL string
}
// NewAuthorizationService 创建授权书服务实例
func NewAuthorizationService(c config.Config, authDocModel model.AuthorizationDocumentModel) *AuthorizationService {
absStoragePath := determineStoragePath()
return &AuthorizationService{
config: c,
authDocModel: authDocModel,
fileStoragePath: absStoragePath,
fileBaseURL: c.Authorization.FileBaseURL, // 从配置文件读取
}
}
// GenerateAuthorizationDocument 生成授权书PDF
func (s *AuthorizationService) GenerateAuthorizationDocument(
ctx context.Context,
userID int64,
orderID int64,
queryID int64,
userInfo map[string]interface{},
) (*model.AuthorizationDocument, error) {
// 1. 生成PDF内容
pdfBytes, err := s.generatePDFContent(userInfo)
if err != nil {
return nil, errors.Wrapf(err, "生成PDF内容失败")
}
// 2. 创建文件存储目录
year := time.Now().Format("2006")
month := time.Now().Format("01")
dirPath := filepath.Join(s.fileStoragePath, year, month)
if err := os.MkdirAll(dirPath, 0755); err != nil {
return nil, errors.Wrapf(err, "创建存储目录失败: %s", dirPath)
}
// 3. 生成文件名和路径
fileName := fmt.Sprintf("auth_%d_%d_%s.pdf", userID, orderID, time.Now().Format("20060102_150405"))
filePath := filepath.Join(dirPath, fileName)
// 只存储相对路径,不包含域名
relativePath := fmt.Sprintf("%s/%s/%s", year, month, fileName)
// 4. 保存PDF文件
if err := os.WriteFile(filePath, pdfBytes, 0644); err != nil {
return nil, errors.Wrapf(err, "保存PDF文件失败: %s", filePath)
}
// 5. 保存到数据库
authDoc := &model.AuthorizationDocument{
UserId: userID,
OrderId: orderID,
QueryId: queryID,
FileName: fileName,
FilePath: filePath,
FileUrl: relativePath, // 只存储相对路径
FileSize: int64(len(pdfBytes)),
FileType: "pdf",
Status: "active",
ExpireTime: sql.NullTime{Valid: false}, // 永久保留,不设置过期时间
}
result, err := s.authDocModel.Insert(ctx, nil, authDoc)
if err != nil {
// 如果数据库保存失败,删除已创建的文件
os.Remove(filePath)
return nil, errors.Wrapf(err, "保存授权书记录失败")
}
authDoc.Id, _ = result.LastInsertId()
logx.Infof("授权书生成成功: userID=%d, orderID=%d, filePath=%s", userID, orderID, filePath)
return authDoc, nil
}
// GetFullFileURL 获取完整的文件访问URL
func (s *AuthorizationService) GetFullFileURL(relativePath string) string {
if relativePath == "" {
return ""
}
return fmt.Sprintf("%s/%s", s.fileBaseURL, relativePath)
}
// ResolveFilePath 根据存储的路径信息解析出本地文件的绝对路径
func (s *AuthorizationService) ResolveFilePath(filePath string, relativePath string) string {
candidates := []string{}
cleanRelative := func(p string) string {
p = strings.TrimPrefix(p, "/")
p = strings.TrimPrefix(p, "\\")
normalized := filepath.ToSlash(p)
if strings.HasPrefix(normalized, "http://") || strings.HasPrefix(normalized, "https://") {
if parsed, err := url.Parse(normalized); err == nil {
normalized = filepath.ToSlash(strings.TrimPrefix(parsed.Path, "/"))
}
}
normalized = strings.TrimPrefix(normalized, "data/authorization_docs/")
normalized = strings.TrimPrefix(normalized, "authorization_docs/")
normalized = strings.TrimPrefix(normalized, "./")
normalized = strings.TrimPrefix(normalized, "../")
return normalized
}
if filePath != "" {
candidates = append(candidates, filePath)
}
if relativePath != "" {
candidates = append(candidates, filepath.Join(s.fileStoragePath, filepath.FromSlash(cleanRelative(relativePath))))
}
if filePath != "" {
cleaned := filepath.Clean(filePath)
if strings.HasPrefix(cleaned, s.fileStoragePath) {
candidates = append(candidates, cleaned)
} else {
trimmed := strings.TrimPrefix(cleaned, "data"+string(os.PathSeparator)+"authorization_docs"+string(os.PathSeparator))
trimmed = strings.TrimPrefix(trimmed, "data/authorization_docs/")
if trimmed != cleaned {
candidates = append(candidates, filepath.Join(s.fileStoragePath, filepath.FromSlash(cleanRelative(trimmed))))
}
if !filepath.IsAbs(cleaned) {
candidates = append(candidates, filepath.Join(s.fileStoragePath, filepath.FromSlash(cleanRelative(cleaned))))
}
}
}
for _, candidate := range candidates {
if candidate == "" {
continue
}
pathCandidate := candidate
if !filepath.IsAbs(pathCandidate) {
if absPath, err := filepath.Abs(pathCandidate); err == nil {
pathCandidate = absPath
}
}
if statErr := checkFileExists(pathCandidate); statErr == nil {
return pathCandidate
}
}
logx.Errorf("授权书文件路径解析失败 filePath=%s fileUrl=%s candidates=%v", filePath, relativePath, candidates)
return ""
}
func determineStoragePath() string {
candidatePaths := []string{
"data/authorization_docs",
"../data/authorization_docs",
"../../data/authorization_docs",
"../../../data/authorization_docs",
}
for _, candidate := range candidatePaths {
absPath, err := filepath.Abs(candidate)
if err != nil {
logx.Errorf("解析授权书存储路径失败: %s, err=%v", candidate, err)
continue
}
if info, err := os.Stat(absPath); err == nil && info.IsDir() {
logx.Infof("授权书存储路径选择: %s", absPath)
return absPath
}
}
// 如果没有现成的目录,使用第一个候选的绝对路径
absPath, err := filepath.Abs(candidatePaths[0])
if err != nil {
logx.Errorf("解析默认授权书存储路径失败,使用相对路径: %s, err=%v", candidatePaths[0], err)
return candidatePaths[0]
}
logx.Infof("授权书存储路径创建: %s", absPath)
return absPath
}
func checkFileExists(path string) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.IsDir() {
return fmt.Errorf("path %s is directory", path)
}
return nil
}
// generatePDFContent 生成PDF内容
func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{}) ([]byte, error) {
// 创建PDF文档
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddPage()
// 添加中文字体支持 - 参考imageService的路径处理方式
fontPaths := []string{
"static/SIMHEI.TTF", // 相对于工作目录的路径与imageService一致
"/app/static/SIMHEI.TTF", // Docker容器内的字体文件
"app/main/api/static/SIMHEI.TTF", // 开发环境备用路径
}
// 尝试添加字体
fontAdded := false
for _, fontPath := range fontPaths {
if _, err := os.Stat(fontPath); err == nil {
pdf.AddUTF8Font("ChineseFont", "", fontPath)
fontAdded = true
logx.Infof("成功加载字体: %s", fontPath)
break
} else {
logx.Debugf("字体文件不存在: %s, 错误: %v", fontPath, err)
}
}
// 如果没有找到字体文件,使用默认字体,并记录警告
if !fontAdded {
pdf.SetFont("Arial", "", 12)
logx.Errorf("未找到中文字体文件使用默认Arial字体可能无法正确显示中文")
} else {
// 设置默认字体
pdf.SetFont("ChineseFont", "", 12)
}
// 获取用户信息
name := getUserInfoString(userInfo, "name")
idCard := getUserInfoString(userInfo, "id_card")
// 生成当前日期
currentDate := time.Now().Format("2006年1月2日")
// 设置标题样式 - 大字体、居中
if fontAdded {
pdf.SetFont("ChineseFont", "", 20) // 使用20号字体
} else {
pdf.SetFont("Arial", "", 20)
}
pdf.CellFormat(0, 15, "授权书", "", 1, "C", false, 0, "")
// 添加空行
pdf.Ln(5)
// 设置正文样式 - 正常字体
if fontAdded {
pdf.SetFont("ChineseFont", "", 12)
} else {
pdf.SetFont("Arial", "", 12)
}
// 构建授权书内容(与前端 Authorization.vue 保持一致)
content := fmt.Sprintf(`沈阳知讯科技有限公司
本人姓名%s身份证号码%s拟向贵司申请业务贵司需要了解本人相关状况用于查询大数据分析报告因此本人特同意并不可撤销的授权
贵司向依法成立的第三方服务商包括但不限于天津津北数字产业发展集团有限公司根据本人提交的信息进行核实并有权通过前述第三方服务机构查询使用本人的身份信息电话号码等查询本人信息包括但不限于学历婚姻资产状况及对信息主体产生负面影响的不良信息出具相关报告
第三方服务商应当在上述处理目的处理方式和个人信息的种类等范围内处理个人信息变更原先的处理目的处理方式的应当依法重新取得您的同意
本人在此声明已充分理解上述授权条款含义知晓并自愿承担上述因收集等本人数据可能会给本人的生活行为评分结果产生不利影响以及该等数据被使用者依法提供给第三方后被他人不当利用的风险但本人仍同意上述授权
特别提示
为了保障您的合法权益请您务必阅读并充分理解与遵守本授权书若您不接受本授权书的任何条款请您立即终止授权贵司已经对上述事宜及其风险向本人做了充分说明本人已知晓并同意
你通过知讯数据APP或代理商推广查询模式自愿支付相应费用用于购买沈阳知讯科技有限公司的大数据报告产品
你向沈阳知讯科技有限公司的支付方式为沈阳知讯科技有限公司及其关联公司的支付宝及微信账户
本授权书一经本人在网上点击勾选同意即完成签署本授权书是本人真实意思表示本人同意承担由此带来的一切法律后果
授权人%s
身份证号%s
签署时间%s`, name, idCard, name, idCard, currentDate)
// 将内容写入PDF
pdf.MultiCell(0, 6, content, "", "", false)
// 生成PDF字节数组
var buf bytes.Buffer
err := pdf.Output(&buf)
if err != nil {
return nil, errors.Wrapf(err, "生成PDF字节数组失败")
}
return buf.Bytes(), nil
}
// getUserInfoString 安全获取用户信息字符串
func getUserInfoString(userInfo map[string]interface{}, key string) string {
if value, exists := userInfo[key]; exists {
if str, ok := value.(string); ok {
return str
}
}
return ""
}
// generatePDFFromTemplate 从模板文件生成PDF内容
func (s *AuthorizationService) generatePDFFromTemplate(templatePath string, data map[string]string) ([]byte, error) {
// 查找模板文件(与字体文件使用相同的路径策略)
tmplPaths := []string{
templatePath,
"/app/" + templatePath,
"app/main/api/" + templatePath,
"../../" + templatePath, // 从 internal/service/ 向上
"../../../" + templatePath, // 从 api/internal/service/ 向上
"../../../../" + templatePath, // 从 app/main/api/internal/service/ 向上
"../../../../../" + templatePath, // 从 bd-server/app/main/api/internal/service/ 向上
"../../static/" + filepath.Base(templatePath), // 从 internal/service/ 找 static/
"../../../static/" + filepath.Base(templatePath), // 从 api/internal/service/ 找 static/
"../../../../static/" + filepath.Base(templatePath), // 从 app/main/api/internal/service/ 找 static/
}
var tmplFile string
for _, p := range tmplPaths {
if _, err := os.Stat(p); err == nil {
tmplFile = p
break
}
}
if tmplFile == "" {
return nil, fmt.Errorf("未找到模板文件: %s", templatePath)
}
// 解析模板文件
tmpl, err := template.ParseFiles(tmplFile)
if err != nil {
return nil, errors.Wrapf(err, "解析模板文件失败: %s", tmplFile)
}
// 渲染模板
var contentBuf bytes.Buffer
if err := tmpl.Execute(&contentBuf, data); err != nil {
return nil, errors.Wrapf(err, "渲染模板失败")
}
renderedContent := contentBuf.String()
// 创建PDF文档
pdf := gofpdf.New("P", "mm", "A4", "")
// 加载中文字体 - 与 generatePDFContent 保持一致的路径策略,并增加更多 fallback
fontPaths := []string{
"static/SIMHEI.TTF", // 相对于工作目录的路径
"/app/static/SIMHEI.TTF", // Docker容器内
"app/main/api/static/SIMHEI.TTF", // 开发环境备用
"../../static/SIMHEI.TTF", // 从 internal/service/ 向上
"../../../static/SIMHEI.TTF", // 从 api/internal/service/ 向上
"../../../../static/SIMHEI.TTF", // 从 app/main/api/internal/service/ 向上
}
fontAdded := false
for _, fontPath := range fontPaths {
if _, err := os.Stat(fontPath); err == nil {
pdf.AddUTF8Font("ChineseFont", "", fontPath)
fontAdded = true
logx.Infof("成功加载字体: %s", fontPath)
break
} else {
logx.Debugf("字体文件不存在: %s, 错误: %v", fontPath, err)
}
}
if !fontAdded {
pdf.SetFont("Arial", "", 11)
logx.Errorf("未找到中文字体文件使用默认Arial字体可能无法正确显示中文")
} else {
pdf.SetFont("ChineseFont", "", 11)
}
// 设置每页Footer回调用于在每页添加水印
// companyName 从模板变量中获取,若无则使用默认值
companyName := data["CompanyName"]
if companyName == "" {
companyName = "沈阳知讯科技有限公司"
}
capturedFontAdded := fontAdded
capturedCompanyName := companyName
pdf.SetFooterFunc(func() {
addWatermarkToPage(pdf, capturedFontAdded, capturedCompanyName)
})
pdf.AddPage()
// 写入渲染后的内容
pdf.MultiCell(0, 6, renderedContent, "", "", false)
// 输出PDF字节
var pdfBuf bytes.Buffer
if err := pdf.Output(&pdfBuf); err != nil {
return nil, errors.Wrapf(err, "生成PDF字节数组失败")
}
return pdfBuf.Bytes(), nil
}
// addWatermarkToPage 给当前页面添加水印(红色公司名,居中单个水印)
func addWatermarkToPage(pdf *gofpdf.Fpdf, fontAdded bool, companyName string) {
if fontAdded {
pdf.SetFont("ChineseFont", "", 40)
} else {
pdf.SetFont("Arial", "", 40)
}
pdf.SetTextColor(255, 200, 200) // 浅红色
// A4页面尺寸 210x297mm水印放在页面正中央
pageWidth, pageHeight := pdf.GetPageSize()
x := pageWidth / 2
y := pageHeight / 2
pdf.TransformBegin()
pdf.TransformRotate(45, x, y)
pdf.Text(x, y, companyName)
pdf.TransformEnd()
pdf.SetTextColor(0, 0, 0)
}
// GenerateIVYZ4Y27AuthorizationBase64 生成IVYZ4Y27专用授权书PDF并返回Base64编码字符串
func (s *AuthorizationService) GenerateIVYZ4Y27AuthorizationBase64(userInfo map[string]interface{}) (string, error) {
name := getUserInfoString(userInfo, "name")
idCard := getUserInfoString(userInfo, "id_card")
if name == "" || idCard == "" {
return "", fmt.Errorf("缺少必要的用户信息name或id_card")
}
// 构建模板变量
data := map[string]string{
"CompanyName": "沈阳知讯科技有限公司",
"Name": name,
"IdCard": idCard,
"Mobile": getUserInfoString(userInfo, "mobile"),
"Date": time.Now().Format("2006年1月2日"),
}
// 从模板生成PDF
pdfBytes, err := s.generatePDFFromTemplate("static/authorization_ivyz4y27.tmpl", data)
if err != nil {
return "", errors.Wrapf(err, "生成IVYZ4Y27授权书PDF失败")
}
// Base64编码
return base64.StdEncoding.EncodeToString(pdfBytes), nil
}