diff --git a/app/main/api/desc/front/authorization.api b/app/main/api/desc/front/authorization.api index 8a82ee9..54d8ed3 100644 --- a/app/main/api/desc/front/authorization.api +++ b/app/main/api/desc/front/authorization.api @@ -47,10 +47,15 @@ type ( DocumentId int64 `json:"documentId" validate:"required"` // 授权书ID } + // DownloadAuthorizationDocumentByNameReq 通过文件名下载授权书请求 + DownloadAuthorizationDocumentByNameReq { + FileName string `path:"fileName" validate:"required"` // 授权书文件名 + } + // DownloadAuthorizationDocumentResp 下载授权书响应 DownloadAuthorizationDocumentResp { FileName string `json:"fileName"` // 文件名 - FileUrl string `json:"fileUrl"` // 文件访问URL + FilePath string `json:"filePath"` // 文件存储路径 } ) @@ -71,4 +76,8 @@ service main { // 下载授权书文件 @handler DownloadAuthorizationDocument get /authorization/download/:documentId (DownloadAuthorizationDocumentReq) returns (DownloadAuthorizationDocumentResp) + + // 通过文件名下载授权书文件 + @handler DownloadAuthorizationDocumentByName + get /authorization/download/file/:fileName (DownloadAuthorizationDocumentByNameReq) returns (DownloadAuthorizationDocumentResp) } diff --git a/app/main/api/internal/handler/authorization/downloadauthorizationdocumentbynamehandler.go b/app/main/api/internal/handler/authorization/downloadauthorizationdocumentbynamehandler.go new file mode 100644 index 0000000..916abc0 --- /dev/null +++ b/app/main/api/internal/handler/authorization/downloadauthorizationdocumentbynamehandler.go @@ -0,0 +1,67 @@ +package authorization + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "os" + "strconv" + + "tydata-server/app/main/api/internal/logic/authorization" + "tydata-server/app/main/api/internal/svc" + "tydata-server/app/main/api/internal/types" + "tydata-server/app/main/model" + "tydata-server/common/result" + "tydata-server/common/xerr" + "tydata-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/rest/httpx" +) + +func DownloadAuthorizationDocumentByNameHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DownloadAuthorizationDocumentByNameReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := authorization.NewDownloadAuthorizationDocumentByNameLogic(r.Context(), svcCtx) + resp, err := l.DownloadAuthorizationDocumentByName(&req) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + httpx.WriteJson(w, http.StatusNotFound, result.Error(xerr.CUSTOM_ERROR, "授权书不存在")) + return + } + result.HttpResult(r, w, nil, err) + return + } + + file, openErr := os.Open(resp.FilePath) + if openErr != nil { + logx.Errorf("打开授权书文件失败: fileName=%s, filePath=%s, error=%v", req.FileName, resp.FilePath, openErr) + result.HttpResult(r, w, nil, errors.New("授权书文件不存在")) + return + } + defer file.Close() + + fileInfo, statErr := file.Stat() + if statErr != nil { + logx.Errorf("读取授权书文件信息失败: fileName=%s, filePath=%s, error=%v", req.FileName, resp.FilePath, statErr) + result.HttpResult(r, w, nil, errors.New("授权书文件不可用")) + return + } + + escapedName := url.PathEscape(resp.FileName) + w.Header().Set("Content-Type", "application/pdf") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", escapedName, escapedName)) + w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)) + + http.ServeContent(w, r, resp.FileName, fileInfo.ModTime(), file) + } +} diff --git a/app/main/api/internal/handler/authorization/downloadauthorizationdocumenthandler.go b/app/main/api/internal/handler/authorization/downloadauthorizationdocumenthandler.go index 491eb32..ee1753c 100644 --- a/app/main/api/internal/handler/authorization/downloadauthorizationdocumenthandler.go +++ b/app/main/api/internal/handler/authorization/downloadauthorizationdocumenthandler.go @@ -1,14 +1,22 @@ package authorization import ( + "errors" + "fmt" "net/http" + "net/url" + "os" + "strconv" "tydata-server/app/main/api/internal/logic/authorization" "tydata-server/app/main/api/internal/svc" "tydata-server/app/main/api/internal/types" + "tydata-server/app/main/model" "tydata-server/common/result" + "tydata-server/common/xerr" "tydata-server/pkg/lzkit/validator" + "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/rest/httpx" ) @@ -25,6 +33,35 @@ func DownloadAuthorizationDocumentHandler(svcCtx *svc.ServiceContext) http.Handl } l := authorization.NewDownloadAuthorizationDocumentLogic(r.Context(), svcCtx) resp, err := l.DownloadAuthorizationDocument(&req) - result.HttpResult(r, w, resp, err) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + httpx.WriteJson(w, http.StatusNotFound, result.Error(xerr.CUSTOM_ERROR, "授权书不存在")) + return + } + result.HttpResult(r, w, nil, err) + return + } + + file, openErr := os.Open(resp.FilePath) + if openErr != nil { + logx.Errorf("打开授权书文件失败: filePath=%s, error=%v", resp.FilePath, openErr) + result.HttpResult(r, w, nil, errors.New("授权书文件不存在")) + return + } + defer file.Close() + + fileInfo, statErr := file.Stat() + if statErr != nil { + logx.Errorf("读取授权书文件信息失败: filePath=%s, error=%v", resp.FilePath, statErr) + result.HttpResult(r, w, nil, errors.New("授权书文件不可用")) + return + } + + escapedName := url.PathEscape(resp.FileName) + w.Header().Set("Content-Type", "application/pdf") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", escapedName, escapedName)) + w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)) + + http.ServeContent(w, r, resp.FileName, fileInfo.ModTime(), file) } } diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go index e347792..9f1baba 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -841,6 +841,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/authorization/download/:documentId", Handler: authorization.DownloadAuthorizationDocumentHandler(serverCtx), }, + { + Method: http.MethodGet, + Path: "/authorization/download/file/:fileName", + Handler: authorization.DownloadAuthorizationDocumentByNameHandler(serverCtx), + }, }, rest.WithPrefix("/api/v1"), ) diff --git a/app/main/api/internal/logic/authorization/downloadauthorizationdocumentbynamelogic.go b/app/main/api/internal/logic/authorization/downloadauthorizationdocumentbynamelogic.go new file mode 100644 index 0000000..c54b675 --- /dev/null +++ b/app/main/api/internal/logic/authorization/downloadauthorizationdocumentbynamelogic.go @@ -0,0 +1,59 @@ +package authorization + +import ( + "context" + "errors" + + "tydata-server/app/main/api/internal/svc" + "tydata-server/app/main/api/internal/types" + "tydata-server/app/main/model" + "tydata-server/common/globalkey" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DownloadAuthorizationDocumentByNameLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDownloadAuthorizationDocumentByNameLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DownloadAuthorizationDocumentByNameLogic { + return &DownloadAuthorizationDocumentByNameLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DownloadAuthorizationDocumentByNameLogic) DownloadAuthorizationDocumentByName(req *types.DownloadAuthorizationDocumentByNameReq) (resp *types.DownloadAuthorizationDocumentResp, err error) { + builder := l.svcCtx.AuthorizationDocumentModel.SelectBuilder(). + Where("file_name = ?", req.FileName). + Where("del_state = ?", globalkey.DelStateNo). + Limit(1) + + authDocs, err := l.svcCtx.AuthorizationDocumentModel.FindAll(l.ctx, builder, "") + if err != nil { + logx.Errorf("根据文件名查询授权书失败: fileName=%s, error=%v", req.FileName, err) + return nil, err + } + + if len(authDocs) == 0 { + logx.Errorf("根据文件名未找到授权书: fileName=%s", req.FileName) + return nil, model.ErrNotFound + } + authDoc := authDocs[0] + + if authDoc.Status != "active" { + logx.Errorf("授权书状态异常: fileName=%s, status=%s", req.FileName, authDoc.Status) + return nil, errors.New("授权书不可用") + } + + filePath := l.svcCtx.AuthorizationService.ResolveFilePath(authDoc.FilePath, authDoc.FileUrl) + + resp = &types.DownloadAuthorizationDocumentResp{ + FileName: authDoc.FileName, + FilePath: filePath, + } + return resp, nil +} diff --git a/app/main/api/internal/logic/authorization/downloadauthorizationdocumentlogic.go b/app/main/api/internal/logic/authorization/downloadauthorizationdocumentlogic.go index db0f975..e3da1ac 100644 --- a/app/main/api/internal/logic/authorization/downloadauthorizationdocumentlogic.go +++ b/app/main/api/internal/logic/authorization/downloadauthorizationdocumentlogic.go @@ -38,13 +38,12 @@ func (l *DownloadAuthorizationDocumentLogic) DownloadAuthorizationDocument(req * return nil, errors.New("授权书不可用") } - // 3. 构建完整文件URL - fullFileURL := l.svcCtx.AuthorizationService.GetFullFileURL(authDoc.FileUrl) + // 3. 构建响应 + filePath := l.svcCtx.AuthorizationService.ResolveFilePath(authDoc.FilePath, authDoc.FileUrl) - // 4. 构建响应 resp = &types.DownloadAuthorizationDocumentResp{ FileName: authDoc.FileName, - FileUrl: fullFileURL, + FilePath: filePath, } return resp, nil diff --git a/app/main/api/internal/queue/paySuccessNotify.go b/app/main/api/internal/queue/paySuccessNotify.go index 13efc67..262abdb 100644 --- a/app/main/api/internal/queue/paySuccessNotify.go +++ b/app/main/api/internal/queue/paySuccessNotify.go @@ -2,17 +2,19 @@ package queue import ( "context" + "encoding/hex" + "encoding/json" + "fmt" + "net/url" + "os" + "path" + "regexp" + "strings" "tydata-server/app/main/api/internal/svc" "tydata-server/app/main/api/internal/types" "tydata-server/app/main/model" "tydata-server/pkg/lzkit/crypto" "tydata-server/pkg/lzkit/lzUtils" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "regexp" - "strings" "github.com/hibiken/asynq" "github.com/zeromicro/go-zero/core/logx" @@ -112,9 +114,9 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq. // 将授权书URL添加到解密数据中 if authDoc != nil { - // 生成完整的授权书访问URL - fullAuthDocURL := l.svcCtx.AuthorizationService.GetFullFileURL(authDoc.FileUrl) - userInfo["authorization_url"] = fullAuthDocURL + // 生成授权书下载接口URL + downloadURL := l.buildAuthorizationDownloadURL(authDoc.FileName) + userInfo["authorization_url"] = downloadURL // 重新序列化用户信息 updatedDecryptData, marshalErr := json.Marshal(userInfo) @@ -132,7 +134,7 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq. if updateParamsErr != nil { logx.Errorf("更新查询参数失败: %v", updateParamsErr) } else { - logx.Infof("成功更新查询参数,包含授权书URL: %s", fullAuthDocURL) + logx.Infof("成功更新查询参数,包含授权书URL: %s", downloadURL) } } decryptData = updatedDecryptData @@ -177,6 +179,18 @@ func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq. return nil } +func (l *PaySuccessNotifyUserHandler) buildAuthorizationDownloadURL(fileName string) string { + escapedFileName := url.PathEscape(fileName) + base := l.svcCtx.Config.Authorization.FileBaseURL + if parsed, err := url.Parse(base); err == nil && parsed.Scheme != "" && parsed.Host != "" { + parsed.Path = path.Join("/api/v1/authorization/download/file", escapedFileName) + parsed.RawQuery = "" + parsed.Fragment = "" + return parsed.String() + } + return fmt.Sprintf("/api/v1/authorization/download/file/%s", escapedFileName) +} + // 定义一个中间件函数 func (l *PaySuccessNotifyUserHandler) handleError(ctx context.Context, err error, order *model.Order, query *model.Query) error { logx.Errorf("处理任务失败,原因: %v", err) diff --git a/app/main/api/internal/service/authorizationService.go b/app/main/api/internal/service/authorizationService.go index bf9f43f..536e1f2 100644 --- a/app/main/api/internal/service/authorizationService.go +++ b/app/main/api/internal/service/authorizationService.go @@ -5,8 +5,10 @@ import ( "context" "database/sql" "fmt" + "net/url" "os" "path/filepath" + "strings" "time" "tydata-server/app/main/api/internal/config" "tydata-server/app/main/model" @@ -25,11 +27,13 @@ type AuthorizationService struct { // NewAuthorizationService 创建授权书服务实例 func NewAuthorizationService(c config.Config, authDocModel model.AuthorizationDocumentModel) *AuthorizationService { + absStoragePath := determineStoragePath() + return &AuthorizationService{ config: c, authDocModel: authDocModel, - fileStoragePath: "data/authorization_docs", // 使用相对路径,兼容开发环境 - fileBaseURL: c.Authorization.FileBaseURL, // 从配置文件读取 + fileStoragePath: absStoragePath, + fileBaseURL: c.Authorization.FileBaseURL, // 从配置文件读取 } } @@ -101,6 +105,109 @@ func (s *AuthorizationService) GetFullFileURL(relativePath string) string { 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文档 @@ -113,7 +220,7 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{ "/app/static/SIMHEI.TTF", // Docker容器内的字体文件 "app/main/api/static/SIMHEI.TTF", // 开发环境备用路径 } - + // 尝试添加字体 fontAdded := false for _, fontPath := range fontPaths { @@ -150,17 +257,17 @@ func (s *AuthorizationService) generatePDFContent(userInfo map[string]interface{ 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) } - + // 构建授权书内容(去掉标题部分) content := fmt.Sprintf(`海南天远大数据科技有限公司: 本人%s拟向贵司申请大数据分析报告查询业务,贵司需要了解本人相关状况,用于查询大数据分析报告,因此本人同意向贵司提供本人的姓名和手机号等个人信息,并同意贵司向第三方传送上述信息。第三方将使用上述信息核实信息真实情况,查询信用记录,并生成报告。 diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index abf6078..21801c1 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -1231,13 +1231,17 @@ type DirectPushReport struct { Last30D TimeRangeReport `json:"last30d"` // 近30天数据 } +type DownloadAuthorizationDocumentByNameReq struct { + FileName string `path:"fileName" validate:"required"` // 授权书文件名 +} + type DownloadAuthorizationDocumentReq struct { DocumentId int64 `json:"documentId" validate:"required"` // 授权书ID } type DownloadAuthorizationDocumentResp struct { FileName string `json:"fileName"` // 文件名 - FileUrl string `json:"fileUrl"` // 文件访问URL + FilePath string `json:"filePath"` // 文件存储路径 } type Feature struct { diff --git a/app/main/model/authorizationDocumentModel.go b/app/main/model/authorizationDocumentModel.go index db02124..267470b 100644 --- a/app/main/model/authorizationDocumentModel.go +++ b/app/main/model/authorizationDocumentModel.go @@ -32,12 +32,8 @@ func NewAuthorizationDocumentModel(conn sqlx.SqlConn, c cache.CacheConf) Authori // FindByOrderId 根据订单ID查询授权书列表 func (m *customAuthorizationDocumentModel) FindByOrderId(ctx context.Context, orderId int64) ([]*AuthorizationDocument, error) { query := `SELECT * FROM authorization_document WHERE order_id = ? AND del_state = 0 ORDER BY create_time DESC` - + var authDocs []*AuthorizationDocument err := m.QueryRowsNoCacheCtx(ctx, &authDocs, query, orderId) - if err != nil { - return nil, err - } - - return authDocs, nil + return authDocs, err }