312 lines
10 KiB
Go
312 lines
10 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bdrp-server/app/main/api/internal/config"
|
|||
|
|
"bdrp-server/app/main/model"
|
|||
|
|
"bytes"
|
|||
|
|
"context"
|
|||
|
|
"database/sql"
|
|||
|
|
"fmt"
|
|||
|
|
"net/url"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"strings"
|
|||
|
|
"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 ""
|
|||
|
|
}
|