Compare commits

...

61 Commits

Author SHA1 Message Date
cjy
7564af8220 fix:如果新买一个套餐,原有套餐的直接通过,不需要消耗额度 2026-01-27 13:24:35 +08:00
cjy
4ce735d4de fix: 买套餐之后,旧套餐期间的数据分析和竞品报告不让看 2026-01-26 15:58:58 +08:00
cjy
c6f4385991 fix: 修改报告名 2026-01-26 15:27:32 +08:00
cjy
c0ea800511 fix: 修复套餐过期导致的错误 2026-01-26 15:01:01 +08:00
cjy
6c7e27ce78 fix: 修复检查余额的逻辑 2026-01-26 11:08:02 +08:00
cjy
7ba69a3afd fix: 修改批量导入竞品报告的模板
1.批量导入竞品报告的时候自动生成pdf
2. 增加生成pdf的检查
2026-01-26 10:44:43 +08:00
cjy
e7f90a304b fix: 移除检查 2026-01-26 10:02:51 +08:00
cjy
d20a124e14 fix: 修复逻辑 2026-01-26 09:47:21 +08:00
cjy
cccee7f86e fix:修复逻辑 2026-01-26 09:24:35 +08:00
cjy
adf3b5ee89 fix: 竞品报告需要扣减余额,以及修改错误提示语 2026-01-23 16:59:44 +08:00
cjy
ffdc047d15 fix: h5艺人确认增加校验 2026-01-23 16:30:06 +08:00
cjy
89bf59d878 fix: 修改竞品报告的prompts 2026-01-23 14:43:39 +08:00
cjy
6845ea1736 fix: 修复逻辑错误 2026-01-23 14:34:30 +08:00
cjy
351709d08e fix: 修复页面空间不足时,图片展示不完全的问题 2026-01-23 14:30:19 +08:00
cjy
11c8d63789 fix:修改生成pdf的文件名 2026-01-23 11:14:14 +08:00
cjy
c8397bcfe9 fix: 修改生成竞品报告的prompts 2026-01-23 10:18:44 +08:00
cjy
91f7b54581 fix: 修改一下promps,不让ai输出markdown格式 2026-01-23 09:28:22 +08:00
cjy
4d8e91822f update: 更新pb 2026-01-22 11:54:00 +08:00
cjy
e9fa67ae00 fix: 修复pdf渲染图片只能渲染jpg格式的问题 2026-01-21 16:09:44 +08:00
cjy
4d82fb6f96 fix: 将ai生成的url存到阿里云上 2026-01-21 15:59:26 +08:00
cjy
445eb6a751 fix: 移除一些表情 2026-01-21 15:36:57 +08:00
cjy
ee9bbde2a7 fix: 修改报错提示语,移除一个没用的请求头 2026-01-21 14:58:41 +08:00
cjy
3180c3c476 fix:增加一个请求头 2026-01-21 14:12:18 +08:00
cjy
3c11449f6d 删除没用的接口 2026-01-20 17:15:43 +08:00
cjy
4f9a38693d feat: 更新pb文件 2026-01-19 11:48:46 +08:00
cjy
d097e9a20e feat:增加ai生成竞品报告接口 2026-01-15 16:35:47 +08:00
cjy
a2d46c4463 fix: 不通过链接分析视频文件大小 2026-01-15 15:33:11 +08:00
cjy
222b294101 feat:增加多模态ai测试接口 2026-01-15 14:13:00 +08:00
cjy
9170c77e32 feat: 增加获取url链接的文件大小的工具函数 2026-01-15 14:11:49 +08:00
jiaji.H
76a08f9ad8 Updata:解决冲突 2026-01-15 11:17:26 +08:00
jiaji.H
8688bd6abd Updata:更新pb文件 2026-01-15 11:09:17 +08:00
cjy
e9cd6876c2 feat: 增加分布式锁,防止重复提交 2026-01-14 18:56:59 +08:00
cjy
a4bff4284a update: 更新pb 2026-01-14 17:43:00 +08:00
cjy
d811235da5 fix:移除注释 2026-01-14 17:09:13 +08:00
cjy
bdf3fa6144 feat:pdf上传完之后就删除 2026-01-14 17:08:55 +08:00
cjy
dfb2e5e037 fix: 修复逻辑错误 2026-01-14 17:04:41 +08:00
cjy
6b52775913 feat:增加自动生成pdf功能 2026-01-14 16:47:52 +08:00
jiaji.H
8d36aeb751 Updata:解决冲突 2026-01-14 13:42:58 +08:00
cjy
0226b0af12 feat: 增加艺人确认竞品报告路由 2026-01-14 11:59:40 +08:00
cjy
ae76287088 feat: 把单个艺人竞品报告的接口的分开 2026-01-14 11:58:20 +08:00
cjy
13fa87ec2b feat:source固定为1 2026-01-14 09:42:46 +08:00
cjy
fbf24995b8 feat:减少前端需要传的字段 2026-01-13 16:40:25 +08:00
cjy
145935ba04 fix: 打开批量导入的接口 2026-01-13 16:22:10 +08:00
cjy
a3e617a87f feat:批量导入增加校验竞品报告的余额 2026-01-13 16:20:00 +08:00
cjy
8fa9f89db9 feat:批量导入竞品报告 2026-01-13 16:15:06 +08:00
cjy
e6ca737fb1 feat:增加一个作品列表接口 2026-01-13 15:19:13 +08:00
cjy
49caaa73c6 feat: 更新pb 2026-01-13 15:15:11 +08:00
cjy
79f37993c1 feat:增加竞品报告导入模板 2026-01-13 11:27:44 +08:00
cjy
b079b597c3 feat: 增加检验竞品报告余额 2026-01-13 11:24:50 +08:00
jiaji.H
c5f7903c6c Updata:解决冲突 2026-01-13 09:44:01 +08:00
jiaji.H
065db45cdc Updata:更新pb文件 2026-01-13 09:43:37 +08:00
jiaji.H
3d67be4e09 Updata:解决冲突 2026-01-13 09:39:13 +08:00
jiaji.H
e75e6b7ce9 Updata:增加竞品数pb 2026-01-13 09:36:01 +08:00
cjy
789af8f0db Merge branch 'main' into feat-cjy-report 2026-01-13 09:33:35 +08:00
cjy
09f598a1f4 feat: 增加自动审批 2026-01-12 18:34:56 +08:00
cjy
a6e5d38a43 feat: 增加导出excel接口 2026-01-12 14:45:52 +08:00
cjy
26c59ee54c feat: 增加竞品报告相关路由 2026-01-12 14:34:15 +08:00
cjy
17215e758b feat: 更新pb文件 2026-01-12 14:30:35 +08:00
jiaji.H
daad39d53c Updata:更新表格样式 2026-01-05 11:51:20 +08:00
jiaji.H
b52190f021 Updata:补充导出数据字段 2026-01-05 11:46:02 +08:00
jiaji.H
2a38ed44b0 Updata:增加竞品数类型数据 2026-01-04 15:04:09 +08:00
25 changed files with 15634 additions and 7570 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-triple. DO NOT EDIT.
// versions:
// - protoc-gen-go-triple v1.0.8
// - protoc v6.32.0--rc2
// - protoc v3.21.1
// source: pb/fiee/cast.proto
package cast
@ -52,6 +52,9 @@ type CastClient interface {
UpdateWorkPlatformInfo(ctx context.Context, in *UpdateWorkPlatformInfoReq, opts ...grpc_go.CallOption) (*UpdateWorkPlatformInfoResp, common.ErrorWithAttachment)
UpdateWorkPublishLog(ctx context.Context, in *UpdateWorkPublishLogReq, opts ...grpc_go.CallOption) (*emptypb.Empty, common.ErrorWithAttachment)
RefreshWorkList(ctx context.Context, in *RefreshWorkListReq, opts ...grpc_go.CallOption) (*RefreshWorkListResp, common.ErrorWithAttachment)
WorkResource(ctx context.Context, in *WorkResourceReq, opts ...grpc_go.CallOption) (*WorkResourceResp, common.ErrorWithAttachment)
UpdateWorkResource(ctx context.Context, in *UpdateWorkResourceReq, opts ...grpc_go.CallOption) (*UpdateWorkResourceResp, common.ErrorWithAttachment)
UpdateMediaAccStatus(ctx context.Context, in *UpdateMediaAccStatusReq, opts ...grpc_go.CallOption) (*emptypb.Empty, common.ErrorWithAttachment)
OAuthAccount(ctx context.Context, in *OAuthAccountReq, opts ...grpc_go.CallOption) (*OAuthAccountResp, common.ErrorWithAttachment)
OAuthAccountV2(ctx context.Context, in *OAuthAccountV2Req, opts ...grpc_go.CallOption) (*OAuthAccountV2Resp, common.ErrorWithAttachment)
OAuthCodeToToken(ctx context.Context, in *OAuthCodeToTokenReq, opts ...grpc_go.CallOption) (*OAuthCodeToTokenResp, common.ErrorWithAttachment)
@ -160,6 +163,9 @@ type CastClientImpl struct {
UpdateWorkPlatformInfo func(ctx context.Context, in *UpdateWorkPlatformInfoReq) (*UpdateWorkPlatformInfoResp, error)
UpdateWorkPublishLog func(ctx context.Context, in *UpdateWorkPublishLogReq) (*emptypb.Empty, error)
RefreshWorkList func(ctx context.Context, in *RefreshWorkListReq) (*RefreshWorkListResp, error)
WorkResource func(ctx context.Context, in *WorkResourceReq) (*WorkResourceResp, error)
UpdateWorkResource func(ctx context.Context, in *UpdateWorkResourceReq) (*UpdateWorkResourceResp, error)
UpdateMediaAccStatus func(ctx context.Context, in *UpdateMediaAccStatusReq) (*emptypb.Empty, error)
OAuthAccount func(ctx context.Context, in *OAuthAccountReq) (*OAuthAccountResp, error)
OAuthAccountV2 func(ctx context.Context, in *OAuthAccountV2Req) (*OAuthAccountV2Resp, error)
OAuthCodeToToken func(ctx context.Context, in *OAuthCodeToTokenReq) (*OAuthCodeToTokenResp, error)
@ -375,6 +381,24 @@ func (c *castClient) RefreshWorkList(ctx context.Context, in *RefreshWorkListReq
return out, c.cc.Invoke(ctx, "/"+interfaceKey+"/RefreshWorkList", in, out)
}
func (c *castClient) WorkResource(ctx context.Context, in *WorkResourceReq, opts ...grpc_go.CallOption) (*WorkResourceResp, common.ErrorWithAttachment) {
out := new(WorkResourceResp)
interfaceKey := ctx.Value(constant.InterfaceKey).(string)
return out, c.cc.Invoke(ctx, "/"+interfaceKey+"/WorkResource", in, out)
}
func (c *castClient) UpdateWorkResource(ctx context.Context, in *UpdateWorkResourceReq, opts ...grpc_go.CallOption) (*UpdateWorkResourceResp, common.ErrorWithAttachment) {
out := new(UpdateWorkResourceResp)
interfaceKey := ctx.Value(constant.InterfaceKey).(string)
return out, c.cc.Invoke(ctx, "/"+interfaceKey+"/UpdateWorkResource", in, out)
}
func (c *castClient) UpdateMediaAccStatus(ctx context.Context, in *UpdateMediaAccStatusReq, opts ...grpc_go.CallOption) (*emptypb.Empty, common.ErrorWithAttachment) {
out := new(emptypb.Empty)
interfaceKey := ctx.Value(constant.InterfaceKey).(string)
return out, c.cc.Invoke(ctx, "/"+interfaceKey+"/UpdateMediaAccStatus", in, out)
}
func (c *castClient) OAuthAccount(ctx context.Context, in *OAuthAccountReq, opts ...grpc_go.CallOption) (*OAuthAccountResp, common.ErrorWithAttachment) {
out := new(OAuthAccountResp)
interfaceKey := ctx.Value(constant.InterfaceKey).(string)
@ -780,6 +804,9 @@ type CastServer interface {
UpdateWorkPlatformInfo(context.Context, *UpdateWorkPlatformInfoReq) (*UpdateWorkPlatformInfoResp, error)
UpdateWorkPublishLog(context.Context, *UpdateWorkPublishLogReq) (*emptypb.Empty, error)
RefreshWorkList(context.Context, *RefreshWorkListReq) (*RefreshWorkListResp, error)
WorkResource(context.Context, *WorkResourceReq) (*WorkResourceResp, error)
UpdateWorkResource(context.Context, *UpdateWorkResourceReq) (*UpdateWorkResourceResp, error)
UpdateMediaAccStatus(context.Context, *UpdateMediaAccStatusReq) (*emptypb.Empty, error)
OAuthAccount(context.Context, *OAuthAccountReq) (*OAuthAccountResp, error)
OAuthAccountV2(context.Context, *OAuthAccountV2Req) (*OAuthAccountV2Resp, error)
OAuthCodeToToken(context.Context, *OAuthCodeToTokenReq) (*OAuthCodeToTokenResp, error)
@ -935,6 +962,15 @@ func (UnimplementedCastServer) UpdateWorkPublishLog(context.Context, *UpdateWork
func (UnimplementedCastServer) RefreshWorkList(context.Context, *RefreshWorkListReq) (*RefreshWorkListResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method RefreshWorkList not implemented")
}
func (UnimplementedCastServer) WorkResource(context.Context, *WorkResourceReq) (*WorkResourceResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method WorkResource not implemented")
}
func (UnimplementedCastServer) UpdateWorkResource(context.Context, *UpdateWorkResourceReq) (*UpdateWorkResourceResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateWorkResource not implemented")
}
func (UnimplementedCastServer) UpdateMediaAccStatus(context.Context, *UpdateMediaAccStatusReq) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateMediaAccStatus not implemented")
}
func (UnimplementedCastServer) OAuthAccount(context.Context, *OAuthAccountReq) (*OAuthAccountResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method OAuthAccount not implemented")
}
@ -1819,6 +1855,93 @@ func _Cast_RefreshWorkList_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler)
}
func _Cast_WorkResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc_go.UnaryServerInterceptor) (interface{}, error) {
in := new(WorkResourceReq)
if err := dec(in); err != nil {
return nil, err
}
base := srv.(dubbo3.Dubbo3GrpcService)
args := []interface{}{}
args = append(args, in)
md, _ := metadata.FromIncomingContext(ctx)
invAttachment := make(map[string]interface{}, len(md))
for k, v := range md {
invAttachment[k] = v
}
invo := invocation.NewRPCInvocation("WorkResource", args, invAttachment)
if interceptor == nil {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
info := &grpc_go.UnaryServerInfo{
Server: srv,
FullMethod: ctx.Value("XXX_TRIPLE_GO_INTERFACE_NAME").(string),
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
return interceptor(ctx, in, info, handler)
}
func _Cast_UpdateWorkResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc_go.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateWorkResourceReq)
if err := dec(in); err != nil {
return nil, err
}
base := srv.(dubbo3.Dubbo3GrpcService)
args := []interface{}{}
args = append(args, in)
md, _ := metadata.FromIncomingContext(ctx)
invAttachment := make(map[string]interface{}, len(md))
for k, v := range md {
invAttachment[k] = v
}
invo := invocation.NewRPCInvocation("UpdateWorkResource", args, invAttachment)
if interceptor == nil {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
info := &grpc_go.UnaryServerInfo{
Server: srv,
FullMethod: ctx.Value("XXX_TRIPLE_GO_INTERFACE_NAME").(string),
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
return interceptor(ctx, in, info, handler)
}
func _Cast_UpdateMediaAccStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc_go.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateMediaAccStatusReq)
if err := dec(in); err != nil {
return nil, err
}
base := srv.(dubbo3.Dubbo3GrpcService)
args := []interface{}{}
args = append(args, in)
md, _ := metadata.FromIncomingContext(ctx)
invAttachment := make(map[string]interface{}, len(md))
for k, v := range md {
invAttachment[k] = v
}
invo := invocation.NewRPCInvocation("UpdateMediaAccStatus", args, invAttachment)
if interceptor == nil {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
info := &grpc_go.UnaryServerInfo{
Server: srv,
FullMethod: ctx.Value("XXX_TRIPLE_GO_INTERFACE_NAME").(string),
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
return interceptor(ctx, in, info, handler)
}
func _Cast_OAuthAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc_go.UnaryServerInterceptor) (interface{}, error) {
in := new(OAuthAccountReq)
if err := dec(in); err != nil {
@ -3745,6 +3868,18 @@ var Cast_ServiceDesc = grpc_go.ServiceDesc{
MethodName: "RefreshWorkList",
Handler: _Cast_RefreshWorkList_Handler,
},
{
MethodName: "WorkResource",
Handler: _Cast_WorkResource_Handler,
},
{
MethodName: "UpdateWorkResource",
Handler: _Cast_UpdateWorkResource_Handler,
},
{
MethodName: "UpdateMediaAccStatus",
Handler: _Cast_UpdateMediaAccStatus_Handler,
},
{
MethodName: "OAuthAccount",
Handler: _Cast_OAuthAccount_Handler,

Binary file not shown.

5
go.mod
View File

@ -114,8 +114,10 @@ require (
github.com/fonchain_enterprise/utils/objstorage v0.0.0-00010101000000-000000000000
github.com/gin-contrib/pprof v1.4.0
github.com/go-redis/redis v6.15.9+incompatible
github.com/google/uuid v1.6.0
github.com/mholt/archiver v3.1.1+incompatible
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/phpdave11/gofpdf v1.4.3
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/samber/lo v1.52.0
github.com/shopspring/decimal v1.4.0
@ -165,7 +167,6 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/golang/mock v1.5.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@ -180,7 +181,7 @@ require (
github.com/nxadm/tail v1.4.11 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.18.1 // indirect
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 // indirect
github.com/phpdave11/gofpdi v1.0.15 // indirect
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/polarismesh/polaris-go v1.1.0 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect

9
go.sum
View File

@ -168,6 +168,7 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
@ -583,6 +584,7 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
@ -747,8 +749,11 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 h1:zyWXQ6vu27ETMpYsEMAsisQ+GqJ4e1TPvSNfdOPF0no=
github.com/phpdave11/gofpdf v1.4.3 h1:M/zHvS8FO3zh9tUd2RCOPEjyuVcs281FCyF22Qlz/IA=
github.com/phpdave11/gofpdf v1.4.3/go.mod h1:MAwzoUIgD3J55u0rxIG2eu37c+XWhBtXSpPAhnQXf/o=
github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/phpdave11/gofpdi v1.0.15 h1:iJazY1BQ07I9s7N5EWjBO1YbhmKfHGxNligUv/Rw4Lc=
github.com/phpdave11/gofpdi v1.0.15/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
@ -827,6 +832,7 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@ -1081,6 +1087,7 @@ golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8H
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=

109
pkg/common/qwen/qwen_vl.go Normal file
View File

@ -0,0 +1,109 @@
package qwen
import (
"encoding/json"
"errors"
"fmt"
modelQwen "fonchain-fiee/pkg/model/qwen"
"fonchain-fiee/pkg/utils"
"go.uber.org/zap"
)
// VL 调用通义千问视觉多模态API支持多个视频、多张图片和文本
func VL(videoURLs []string, imageURLs []string, text string, model string) (resp *modelQwen.VLResponse, err error) {
// 设置默认模型
if model == "" {
model = "qwen3-vl-plus"
}
// 构建内容列表
content := make([]modelQwen.VLContent, 0)
// 添加视频内容支持自定义fps
for _, videoURL := range videoURLs {
fps := 2 // 默认fps为2
content = append(content, modelQwen.VLContent{
Type: "video_url",
VideoURL: &modelQwen.VideoURL{
URL: videoURL,
},
FPS: fps,
})
}
// 添加图片内容
for _, imageURL := range imageURLs {
content = append(content, modelQwen.VLContent{
Type: "image_url",
ImageURL: &modelQwen.ImageURL{
URL: imageURL,
},
})
}
// 添加文本内容
if text != "" {
content = append(content, modelQwen.VLContent{
Type: "text",
Text: text,
})
}
// 构建请求
req := modelQwen.VLRequest{
Model: model,
Messages: []modelQwen.VLMessage{
{
Role: "user",
Content: content,
},
},
}
// 序列化请求
jsonData, err := json.Marshal(req)
if err != nil {
zap.L().Error("VL Marshal failed", zap.Error(err))
return nil, errors.New("序列化请求失败")
}
// 发送请求使用PostBytesHeader获取状态码和响应体
statusCode, body, err := utils.PostBytesHeader(modelQwen.DashscopeVLURL, map[string]interface{}{
"Authorization": "Bearer " + modelQwen.DashscopeAPIKey,
"Content-Type": "application/json",
// "X-DashScope-OssResourceResolve": "enable", // 启用OSS资源解析
}, jsonData)
if err != nil {
zap.L().Error("VL Post failed", zap.Error(err))
return nil, errors.New("请求视觉AI失败")
}
// 检查状态码如果不是200尝试解析错误响应
if statusCode != 200 {
// 尝试解析错误响应
var errorResp struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code"`
} `json:"error"`
}
if err := json.Unmarshal(body, &errorResp); err == nil && errorResp.Error.Message != "" {
zap.L().Error("VL API error", zap.Int("status", statusCode), zap.String("message", errorResp.Error.Message))
return nil, fmt.Errorf("%s", errorResp.Error.Message)
}
// 如果无法解析错误响应,返回通用错误
zap.L().Error("VL API error", zap.Int("status", statusCode), zap.String("body", string(body)))
return nil, fmt.Errorf("接口返回错误")
}
// 解析响应
var result modelQwen.VLResponse
if err = json.Unmarshal(body, &result); err != nil {
zap.L().Error("VL Unmarshal failed", zap.Error(err), zap.String("body", string(body)))
return nil, fmt.Errorf("解析响应失败: %v", err)
}
return &result, nil
}

View File

@ -32,6 +32,8 @@ func InitTasks() error {
err = cm.AddTask("artistAutoConfirmAnalysis", "0 */1 * * * *", ArtistAutoConfirmAnalysisTask)
err = cm.AddTask("refreshWorkAnalysisApprovalStatus", "0 */1 * * * *", RefreshWorkAnalysisApprovalStatusTask)
err = cm.AddTask("artistAutoConfirmReport", "0 */1 * * * *", ArtistAutoConfirmReportTask)
err = cm.AddTask("refreshCompetitiveReportApprovalStatus", "0 */1 * * * *", RefreshCompetitiveReportApprovalStatusTask)
err = cm.AddTask("refreshArtistOrder", "0 */30 * * * *", RefreshArtistOrderTask)
// 每天 00:30 和 12:30 执行 Ayrshare 指标采集任务
@ -500,6 +502,43 @@ func ArtistAutoConfirmAnalysisTask() {
}
}
func ArtistAutoConfirmReportTask() {
now := float64(time.Now().Unix())
opt := redis.ZRangeBy{
Min: fmt.Sprintf("%d", 0),
Max: fmt.Sprintf("%f", now),
}
reportUuids, err := cache.RedisClient.ZRangeByScore(modelCast.AutoConfirmReportQueueKey, opt).Result()
if err != nil {
zap.L().Error("获取到期竞品报告任务失败", zap.Error(err))
return
}
if len(reportUuids) == 0 {
zap.L().Debug("没有到期的竞品报告任务")
return
}
zap.L().Info("发现到期竞品报告任务", zap.Int("count", len(reportUuids)))
for _, reportUuid := range reportUuids {
serverCast.ProcessReportTask(context.Background(), reportUuid)
}
}
func RefreshCompetitiveReportApprovalStatusTask() {
resp, err := service.CastProvider.ListCompetitiveReport(context.Background(), &cast.ListCompetitiveReportReq{
Page: 1,
StatusList: []uint32{2}, // 状态为2表示待审批
PageSize: 999999,
})
if err != nil {
log.Printf("获取竞品报告列表失败: %v", err)
return
}
if resp.Data == nil || len(resp.Data) == 0 {
return
}
serverCast.RefreshCompetitiveReportApproval(nil, resp.Data)
}
// AyrshareMetricsCollectorTask Ayrshare 指标采集定时任务(每天 00:30 和 12:30 执行)
func AyrshareMetricsCollectorTask() {
serverCast.ExecuteAyrshareMetricsCollector()

View File

@ -13,7 +13,6 @@ type UserWorkAnalysisConfirmReq struct {
}
type GetBundleBalanceListResp struct {
Total int64 `protobuf:"varint,1,opt,name=total,proto3" json:"total"`
Data []*BundleBalanceItem `protobuf:"bytes,2,rep,name=data,proto3" json:"data"`
}
@ -33,6 +32,8 @@ type BundleBalanceItem struct {
ImageConsumptionNumber int32 `protobuf:"varint,12,opt,name=imageConsumptionNumber,proto3" json:"imageConsumptionNumber"`
DataAnalysisNumber int32 `protobuf:"varint,13,opt,name=dataAnalysisNumber,proto3" json:"dataAnalysisNumber"`
DataAnalysisConsumptionNumber int32 `protobuf:"varint,14,opt,name=dataAnalysisConsumptionNumber,proto3" json:"dataAnalysisConsumptionNumber"`
ExpansionPacksNumber int32 `protobuf:"varint,15,opt,name=expansionPacksNumber,proto3" json:"expansionPacksNumber"`
Bought int32 `protobuf:"varint,16,opt,name=bought,proto3" json:"bought"`
CompetitiveNumber int32 `protobuf:"varint,15,opt,name=competitiveNumber,proto3" json:"competitiveNumber"`
CompetitiveConsumptionNumber int32 `protobuf:"varint,16,opt,name=competitiveConsumptionNumber,proto3" json:"competitiveConsumptionNumber"`
ExpansionPacksNumber int32 `protobuf:"varint,17,opt,name=expansionPacksNumber,proto3" json:"expansionPacksNumber"`
Bought int32 `protobuf:"varint,18,opt,name=bought,proto3" json:"bought"`
}

View File

@ -8,10 +8,11 @@ type SyncAsProfileReq struct {
// 定义枚举值
const (
BalanceTypeAccountValue BalanceTypeEnum = 1
BalanceTypeImageValue BalanceTypeEnum = 2
BalanceTypeVideoValue BalanceTypeEnum = 3
BalanceTypeDataValue BalanceTypeEnum = 4
BalanceTypeAccountValue BalanceTypeEnum = 1 //账号
BalanceTypeImageValue BalanceTypeEnum = 2 //图文
BalanceTypeVideoValue BalanceTypeEnum = 3 //视频
BalanceTypeDataValue BalanceTypeEnum = 4 //数据分析
BalanceTypeCompetitiveValue BalanceTypeEnum = 5 //竞品数
)
var PlatformNameKv = map[uint32]string{

View File

@ -21,6 +21,9 @@ const (
AutoConfirmAnalysisQueueKey = "auto_confirm:analysis:queue"
AutoConfirmAnalysisLockKey = "auto_confirm:analysis:lock:%s"
AutoConfirmReportQueueKey = "auto_confirm:report:queue"
AutoConfirmReportLockKey = "auto_confirm:report:lock:%s"
// AyrshareMetricsCollectorLockKey Ayrshare 指标采集任务锁
AyrshareMetricsCollectorLockKey = "ayrshare:metrics:collector:lock"
)

View File

@ -4,6 +4,7 @@ const (
DashscopeAPIKey string = "sk-5ae9df5d3bcf4755ad5d12012058a2e7"
DashscopeText2ImageURL string = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis"
DashscopeEditImageURL string = "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/image-synthesis"
DashscopeVLURL string = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
)
// QwenImageRequest 通义千问文生图请求

47
pkg/model/qwen/qwen_vl.go Normal file
View File

@ -0,0 +1,47 @@
package qwen
// VLContent 视觉多模态内容结构,支持文本、图片和视频
type VLContent struct {
Type string `json:"type"` // text, image_url, video_url
Text string `json:"text,omitempty"` // type=text 时使用
ImageURL *ImageURL `json:"image_url,omitempty"` // type=image_url 时使用
VideoURL *VideoURL `json:"video_url,omitempty"` // type=video_url 时使用
FPS int `json:"fps,omitempty"` // type=video_url 时可选,视频帧率
}
// VideoURL 视频URL结构
type VideoURL struct {
URL string `json:"url"`
}
// VLRequest 视觉多模态请求结构
type VLRequest struct {
Model string `json:"model"` // 模型名称,如 qwen3-vl-plus
Messages []VLMessage `json:"messages"` // 消息列表
Seed int64 `json:"seed,omitempty"` // 随机种子
EnableSearch bool `json:"enable_search,omitempty"` // 是否启用搜索
}
// VLMessage 视觉多模态消息结构
type VLMessage struct {
Role string `json:"role"` // user, assistant, system
Content []VLContent `json:"content"` // 内容列表,可包含文本、图片、视频
}
// VLResponse 视觉多模态响应结构
type VLResponse struct {
Choices []VLChoice `json:"choices"`
Model string `json:"model,omitempty"`
ID string `json:"id,omitempty"`
}
// VLChoice 视觉多模态选择结果
type VLChoice struct {
Message struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
Role string `json:"role"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
Index int `json:"index,omitempty"`
}

View File

@ -33,18 +33,41 @@ func AnalysisRouter(r *gin.RouterGroup) {
analysis.POST("update-approval-id", serviceCast.UpdateWorkAnalysisApprovalID) // 更新作品分析审批ID
analysis.POST("trigger-ayrshare-metrics", serviceCast.TriggerAyrshareMetricsCollector) // 手动触发 Ayrshare 指标采集任务
}
competitiveReport := r.Group("report")
competitiveReport.Use(middleware.CheckWebLogin(service.AccountProvider))
{
competitiveReport.POST("create", serviceCast.CreateCompetitiveReport) // 创建竞品报告
competitiveReport.POST("import-batch", serviceCast.ImportCompetitiveReportBatch) // 批量导入竞品报告
competitiveReport.POST("update-status", serviceCast.UpdateCompetitiveReportStatus) // 更新竞品报告状态
competitiveReport.POST("detail", serviceCast.GetCompetitiveReport) // 获取竞品报告详情
competitiveReport.POST("list", serviceCast.ListCompetitiveReport) // 获取竞品报告列表
competitiveReport.POST("single-list", serviceCast.ListCompetitiveReportByArtistUuid) // 根据艺人UUID获取竞品报告列表
competitiveReport.POST("delete", serviceCast.DeleteCompetitiveReport) // 删除竞品报告
competitiveReport.POST("update-approval-id", serviceCast.UpdateCompetitiveReportApprovalID) // 更新竞品报告审批ID
competitiveReport.POST("count-by-work-uuids", serviceCast.CountCompetitiveReportByWorkUuids) // 根据作品UUID统计竞品报告数量
competitiveReport.POST("export-list", serviceCast.ListCompetitiveReportExport) // 竞品报告列表导出
competitiveReport.POST("export-single-list", serviceCast.ListCompetitiveReportSingleExport) // 竞品报告单个列表导出
}
// 员工任务相关路由需要App登录验证
analysisAppRoute := r.Group("app/analysis")
analysisAppRoute.Use(middleware.CheckLogin(service.AccountFieeProvider))
{
analysisAppRoute.POST("list", serviceCast.ListWorkAnalysis) // 作品列表
analysisAppRoute.POST("list", serviceCast.ListWorkAnalysisForApp) // 作品列表
analysisAppRoute.POST("detail", serviceCast.GetWorkAnalysis) // 作品分析详情
analysisAppRoute.POST("update-status", serviceCast.UpdateWorkAnalysisStatus) // 用户确认
analysisAppRoute.POST("check-balance", serviceCast.CheckBundleBalance) // 检查套餐余量
analysisAppRoute.POST("tobe-confirmed-list", serviceCast.TobeConfirmedList) // 待确认数据列表
analysisAppRoute.POST("work-analysis-confirm", bundle.WorkAnalysisConfirm)
}
competitiveReportAppRoute := r.Group("app/report")
competitiveReportAppRoute.Use(middleware.CheckLogin(service.AccountFieeProvider))
{
competitiveReportAppRoute.POST("detail", serviceCast.GetCompetitiveReportForApp) // 获取竞品报告详情App端
competitiveReportAppRoute.POST("list", serviceCast.ListReportByArtistUuidForApp) // 根据艺人UUID获取竞品报告列表App端
competitiveReportAppRoute.POST("update-status", serviceCast.UpdateCompetitiveReportStatus) // 更新竞品报告状态App端
}
}

View File

@ -47,6 +47,7 @@ func MediaRouter(r *gin.RouterGroup) {
work.POST("remind", serviceCast.Remind)
work.POST("publish-info", serviceCast.PublishInfo)
work.POST("import-batch", serviceCast.ImportWorkBatch)
work.POST("list-published", serviceCast.WorkListPublished)
}
script := auth.Group("script")
@ -85,11 +86,13 @@ func MediaRouter(r *gin.RouterGroup) {
{
aiNoAuth.POST("image-generate", serviceAI.AIImageGenerate)
aiNoAuth.POST("text-generate", serviceAI.AIChat)
aiNoAuth.POST("video-vl", serviceAI.AIVideoVL)
}
aiAuth := auth.Group("ai")
{
aiAuth.POST("one-text", serviceAI.OneText)
aiAuth.POST("more-text", serviceAI.MoreText)
aiAuth.POST("generate-report", serviceAI.AICompetitorReport)
}
social := noAuth.Group("social")

View File

@ -16,28 +16,22 @@ func TaskBenchRouter(r *gin.RouterGroup) {
taskBenchRoute.Use(middleware.CheckWebLogin(service.AccountProvider))
// 任务台管理
{
// 查询待指派任务记录
taskBenchRoute.POST("pending-task-list", taskbench.GetPendingTaskList)
// 待指派任务布局
taskBenchRoute.POST("pending-task-layout", taskbench.GetPendingTaskLayout)
taskBenchRoute.POST("set-pending-task-layout", taskbench.SetPendingTaskLayout)
// 指派某位员工完成某个艺人的任务
// 指派
taskBenchRoute.POST("assign-task", taskbench.AssignTask)
// 批量指派任务
// 批量指派
taskBenchRoute.POST("batch-assign-task", taskbench.BatchAssignTask)
// 中止指派任务根据任务指派记录UUID
// 中止指派
taskBenchRoute.POST("terminate-task-by-uuid", taskbench.TerminateTaskByUUID)
// 批量中止指派任务根据多个任务指派记录UUID
// 批量中止指派
taskBenchRoute.POST("batch-terminate-task", taskbench.BatchTerminateTask)
// 修改待发数量
taskBenchRoute.POST("update-pending-count", taskbench.UpdatePendingCount)
// 查询最近被指派记录
taskBenchRoute.POST("recent-assign-records", taskbench.GetRecentAssignRecords)
@ -62,12 +56,6 @@ func TaskBenchRouter(r *gin.RouterGroup) {
// 员工手动点击完成任务
taskBenchRoute.POST("complete-manually", taskbench.CompleteTaskManually)
// 查询艺人套餐剩余数量
taskBenchRoute.POST("artist-bundle-balance", taskbench.GetArtistBundleBalance)
// 批量查询艺人待上传数量
taskBenchRoute.POST("batch-get-pending-upload", taskbench.GetPendingUploadBreakdown)
// 查询艺人待上传列表
taskBenchRoute.POST("pending-upload-list", taskbench.GetArtistUploadStatsList)
@ -81,12 +69,4 @@ func TaskBenchRouter(r *gin.RouterGroup) {
taskBenchRoute.POST("pending-data-list", taskbench.GetPendingAssign)
}
// 员工任务相关路由需要App登录验证
taskBenchAppRoute := r.Group("task-bench")
taskBenchAppRoute.Use(middleware.CheckLogin(service.AccountFieeProvider))
{
// 员工实际完成任务状态更新
taskBenchAppRoute.POST("update-progress", taskbench.UpdateTaskProgress)
}
}

277
pkg/service/ai/video_vl.go Normal file
View File

@ -0,0 +1,277 @@
package ai
import (
"errors"
"fmt"
"fonchain-fiee/pkg/common/qwen"
"fonchain-fiee/pkg/service"
"strings"
"github.com/gin-gonic/gin"
)
// VideoVLRequest 视频/图片理解请求参数
type VideoVLRequest struct {
Videos []string `json:"videos"` // 视频URL列表
Images []string `json:"images"` // 图片URL列表
Text string `json:"text"` // 可选的文本提示
Model string `json:"model"` // 可选的模型名称,默认使用 qwen3-vl-plus
}
// AIVideoVL AI理解视频/图片接口
func AIVideoVL(ctx *gin.Context) {
var req VideoVLRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
service.Error(ctx, errors.New("参数错误"))
return
}
// 检查是否至少提供了视频或图片
if len(req.Videos) == 0 && len(req.Images) == 0 {
service.Error(ctx, errors.New("至少需要提供一个视频或图片"))
return
}
if len(req.Videos) > 1 {
service.Error(ctx, errors.New("当前只能选一个视频"))
return
}
Prompt := "请你详细描述视频和图片中的内容分别是什么"
// 调用VL函数进行AI理解
result, err := qwen.VL(req.Videos, req.Images, Prompt, req.Model)
if err != nil {
// 检查是否是文件下载超时错误(内容过大)
errMsg := err.Error()
if contains(errMsg, "Download multimodal file timed out") || contains(errMsg, "timed out") {
service.Error(ctx, errors.New("内容过大,请重新选择"))
} else {
service.Error(ctx, errors.New("ai分析帖子内容失败"))
}
return
}
// 返回AI返回的数据
service.Success(ctx, result)
}
// contains 检查字符串是否包含子字符串(不区分大小写)
func contains(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
// CompetitorReportRequest 竞品报告请求参数
type CompetitorReportRequest struct {
Videos []string `json:"videos"` // 视频URL列表
Images []string `json:"images"` // 图片URL列表
TextPrompt string `json:"textPrompt"` // 竞品报告要求文本
ImagePrompt string `json:"imagePrompt"` // 图片URL
Model string `json:"model"` // 可选的模型名称,默认使用 qwen3-vl-plus
}
// CompetitorReportResponse 竞品报告响应数据
type CompetitorReportResponse struct {
ImageURL string `json:"image_url,omitempty"` // 生成的图片URL1024*1024非必须返回
Text string `json:"text,omitempty"` // 竞品报告文本内容,非必须返回
}
// AICompetitorReport 生成竞品报告接口
func AICompetitorReport(ctx *gin.Context) {
var req CompetitorReportRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
service.Error(ctx, errors.New("参数错误"))
return
}
if req.TextPrompt == "" && req.ImagePrompt == "" {
service.Error(ctx, errors.New("文本和图片提示词不能同时为空"))
return
}
// 检查是否至少提供了视频或图片
if len(req.Videos) == 0 && len(req.Images) == 0 {
service.Error(ctx, errors.New("至少需要提供一个视频或图片"))
return
}
if len(req.Videos) > 1 {
service.Error(ctx, errors.New("当前只能选一个视频"))
return
}
// 第一步调用AI理解视频/图片内容
vlPrompt := "请你详细描述这些视频或者这些图片中的内容分别是什么,请详细描述,不要遗漏任何细节"
vlResult, err := qwen.VL(req.Videos, req.Images, vlPrompt, req.Model)
if err != nil {
// 检查是否是文件下载超时错误(内容过大)
errMsg := err.Error()
if contains(errMsg, "Download multimodal file timed out") || contains(errMsg, "timed out") {
service.Error(ctx, errors.New("内容过大,请重新选择"))
} else {
service.Error(ctx, fmt.Errorf("AI理解视频图片失败: %v", err))
}
return
}
// 获取理解后的内容
if len(vlResult.Choices) == 0 {
service.Error(ctx, errors.New("AI理解返回结果为空"))
return
}
vlContent := vlResult.Choices[0].Message.Content
// 定义协程结果结构
type textResult struct {
text string
err error
}
type imageResult struct {
imageURL string
err error
}
// 根据 TextPrompt 和 ImagePrompt 是否为空决定启动哪些协程
needText := req.TextPrompt != ""
needImage := req.ImagePrompt != ""
var textChan chan textResult
var imageChan chan imageResult
// 如果需要生成文本,启动文本生成协程
if needText {
textChan = make(chan textResult, 1)
go func() {
// 构建文本生成提示词:理解内容 + 用户要求
textPrompt := fmt.Sprintf("基于以下视频和图片的内容描述:\n%s\n\n请根据以下要求生成竞品报告注意不要输出markdown格式来进行排版请直接输出纯文本。只需要回复竞品报告的内容其他无关的内容不要输出输出的内容第一行不要标题直接输出竞品报告的正文即可\n我的要求是\n%s", vlContent, req.TextPrompt)
chatReq, err := buildChatRequest(textPrompt, nil)
if err != nil {
textChan <- textResult{err: err}
return
}
chatResp, err := qwen.Chat(*chatReq)
if err != nil {
textChan <- textResult{err: err}
return
}
if len(chatResp.Choices) == 0 {
textChan <- textResult{err: errors.New("文本生成返回结果为空")}
return
}
textChan <- textResult{text: chatResp.Choices[0].Message.Content}
}()
}
// 如果需要生成图片,启动图片生成协程
if needImage {
imageChan = make(chan imageResult, 1)
go func() {
// 先请求聊天获取图片提示词
imagePromptText := fmt.Sprintf("基于以下视频和图片的内容描述:\n%s\n\n请根据以下要求生成竞品报告图片的提示词\n%s", vlContent, req.ImagePrompt)
chatReq, err := buildChatRequest(imagePromptText, nil)
if err != nil {
imageChan <- imageResult{err: err}
return
}
chatResp, err := qwen.Chat(*chatReq)
if err != nil {
imageChan <- imageResult{err: err}
return
}
if len(chatResp.Choices) == 0 {
imageChan <- imageResult{err: errors.New("图片提示词生成返回结果为空")}
return
}
imagePrompt := chatResp.Choices[0].Message.Content
// 生成图片1024*1024基于理解后的内容使用文生图
size := "1024*1024"
resultTask, err := qwen.GenerateTextImage(imagePrompt, size)
if err != nil {
imageChan <- imageResult{err: err}
return
}
if resultTask.Code != "" {
imageChan <- imageResult{err: errors.New("文生图失败: " + resultTask.Message)}
return
}
// 等待图片生成完成
result, err := qwen.ImgTaskResult(resultTask.Output.TaskID)
if err != nil {
imageChan <- imageResult{err: err}
return
}
if result == nil || len(result.Output.Results) == 0 {
imageChan <- imageResult{err: errors.New("图片生成失败")}
return
}
// 返回第一张图片的URL
imageChan <- imageResult{imageURL: result.Output.Results[0].URL}
}()
}
// 等待所有启动的协程完成
var textRes textResult
var imageRes imageResult
// 根据实际启动的协程数量等待结果
if needText && needImage {
// 两个协程都启动了,使用循环等待两个都完成
completed := 0
for completed < 2 {
select {
case textRes = <-textChan:
completed++
case imageRes = <-imageChan:
completed++
}
}
} else if needText {
// 只启动文本生成协程
textRes = <-textChan
} else if needImage {
// 只启动图片生成协程
imageRes = <-imageChan
}
// 处理文本结果(如果生成了文本)
if needText {
if textRes.err != nil {
service.Error(ctx, fmt.Errorf("生成竞品报告文本失败: %v", textRes.err))
return
}
}
// 处理图片结果(如果生成了图片)
if needImage {
if imageRes.err != nil {
service.Error(ctx, fmt.Errorf("生成竞品报告图片失败: %v", imageRes.err))
return
}
}
// 返回结果(只返回实际生成的内容)
result := CompetitorReportResponse{}
if needText {
result.Text = textRes.text
}
if needImage {
result.ImageURL = imageRes.imageURL
}
service.Success(ctx, result)
}

View File

@ -287,7 +287,7 @@ func WorkAnalysisConfirm(c *gin.Context) { // 确认数据分析并扣除余量
fmt.Println("res:", res)
fmt.Println("err:", err)
if err != nil {
service.Error(c, errors.New(common.UpdateWorkStatusFailed))
service.Error(c, errors.New("驳回失败"))
return
}
service.Success(c, res)
@ -302,6 +302,11 @@ func WorkAnalysisConfirm(c *gin.Context) { // 确认数据分析并扣除余量
return
}
if balanceInfoRes.BundleStatus == common.BundleExpired {
service.Error(c, errors.New("套餐已过期"))
return
}
analysisInfoRes, err := service.CastProvider.GetWorkAnalysis(c, &cast.GetWorkAnalysisDetailReq{
Uuid: req.Uuid,
})
@ -313,6 +318,11 @@ func WorkAnalysisConfirm(c *gin.Context) { // 确认数据分析并扣除余量
service.Error(c, errors.New("数据分析不是待确认状态"))
return
}
artistID, _ := strconv.ParseUint(analysisInfoRes.ArtistID, 10, 64)
if artistID != uint64(userInfo.ID) {
service.Error(c, errors.New("非本人数据分析,无法操作"))
return
}
var addBalanceReq bundle.AddBundleBalanceReq
addBalanceReq.UserId = int32(userInfo.ID)
@ -338,7 +348,7 @@ func WorkAnalysisConfirm(c *gin.Context) { // 确认数据分析并扣除余量
fmt.Println("res:", res)
fmt.Println("err:", err)
if err != nil {
service.Error(c, errors.New(common.UpdateWorkStatusFailed))
service.Error(c, errors.New("确认失败"))
return
}
// 如果是艺人手动确认,确认操作后,自动标记为待阅读状态
@ -457,7 +467,8 @@ func writeToExcel(filename string, items []*bundle.BundleBalanceExportItem) erro
"当前可用套餐视频数", "当前可用增值视频数", "当前已用套餐视频数", "当前已用增值视频数", "当前作废套餐视频数", "当前作废增值视频数", "当月新增可用套餐视频数", "当月新增可用增值视频数", "当月使用套餐视频数", "当月使用增值视频数", "当月作废套餐视频数", "当月作废增值视频数",
"当前可用套餐图文数", "当前可用增值图文数", "当前已用套餐图文数", "当前已用增值图文数", "当前作废套餐图文数", "当前作废增值图文数", "当月新增可用套餐图文数", "当月新增可用增值图文数", "当月使用套餐图文数", "当月使用增值图文数", "当月作废套餐图文数", "当月作废增值图文数",
"当前可用套餐数据分析数", "当前可用增值数据分析数", "当前已用套餐数据分析数", "当前已用增值数据分析数", "当前作废套餐数据分析数", "当前作废增值数据分析数", "当月新增可用套餐数据分析数", "当月新增可用增值数据分析数", "当月使用套餐数据分析数", "当月使用增值数据分析数", "当月作废套餐数据分析数", "当月作废增值数据分析数",
"当月手动扩展账号新增数", "当月手动扩展视频新增数", "当月手动扩展图文新增数", "当月手动扩展数据分析新增数", "当月新增手动扩展时长(天)", "当月手动扩展账号使用数", "当月手动扩展视频使用数", "当月手动扩展图文使用数", "当月手动扩展数据分析使用数",
"当前可用套餐竞品数", "当前可用增值竞品数", "当前已用套餐竞品数", "当前已用增值竞品数", "当前作废套餐竞品数", "当前作废增值竞品数", "当月新增可用套餐竞品数", "当月新增可用增值竞品数", "当月使用套餐竞品数", "当月使用增值竞品数", "当月作废套餐竞品数", "当月作废增值竞品数",
"当月手动扩展账号新增数", "当月手动扩展视频新增数", "当月手动扩展图文新增数", "当月手动扩展数据分析新增数", "当月新增手动扩展时长(天)", "当月手动扩展账号使用数", "当月手动扩展视频使用数", "当月手动扩展图文使用数", "当月手动扩展数据分析使用数", "当月手动扩展竞品数", "当月手动扩展竞品使用数",
}
// 写表头
@ -555,21 +566,37 @@ func writeToExcel(filename string, items []*bundle.BundleBalanceExportItem) erro
_ = write(55, int(it.MonthlyInvalidBundleDataAnalysisNumber))
_ = write(56, int(it.MonthlyInvalidIncreaseDataAnalysisNumber))
// 竞品数
_ = write(57, int(it.MonthlyBundleCompetitiveNumber))
_ = write(58, int(it.MonthlyIncreaseCompetitiveNumber))
_ = write(59, int(it.BundleCompetitiveConsumptionNumber))
_ = write(60, int(it.IncreaseCompetitiveConsumptionNumber))
_ = write(61, int(it.InvalidBundleCompetitiveNumber))
_ = write(62, int(it.InvalidIncreaseCompetitiveNumber))
_ = write(63, int(it.MonthlyNewBundleCompetitiveNumber))
_ = write(64, int(it.MonthlyNewIncreaseCompetitiveNumber))
_ = write(65, int(it.MonthlyBundleCompetitiveConsumptionNumber))
_ = write(66, int(it.MonthlyIncreaseCompetitiveConsumptionNumber))
_ = write(67, int(it.MonthlyInvalidBundleCompetitiveNumber))
_ = write(68, int(it.MonthlyInvalidIncreaseCompetitiveNumber))
// 手动扩展类
_ = write(57, int(it.MonthlyNewManualAccountNumber))
_ = write(58, int(it.MonthlyNewManualVideoNumber))
_ = write(59, int(it.MonthlyNewManualImageNumber))
_ = write(60, int(it.MonthlyNewManualDataAnalysisNumber))
_ = write(61, int(it.MonthlyNewDurationNumber))
_ = write(62, int(it.MonthlyManualAccountConsumptionNumber))
_ = write(63, int(it.MonthlyManualVideoConsumptionNumber))
_ = write(64, int(it.MonthlyManualImageConsumptionNumber))
_ = write(65, int(it.MonthlyManualDataAnalysisConsumptionNumber))
_ = write(69, int(it.MonthlyNewManualAccountNumber))
_ = write(70, int(it.MonthlyNewManualVideoNumber))
_ = write(71, int(it.MonthlyNewManualImageNumber))
_ = write(72, int(it.MonthlyNewManualDataAnalysisNumber))
_ = write(73, int(it.MonthlyNewDurationNumber))
_ = write(74, int(it.MonthlyManualAccountConsumptionNumber))
_ = write(75, int(it.MonthlyManualVideoConsumptionNumber))
_ = write(76, int(it.MonthlyManualImageConsumptionNumber))
_ = write(77, int(it.MonthlyManualDataAnalysisConsumptionNumber))
_ = write(78, int(it.MonthlyNewManualCompetitiveNumber))
_ = write(79, int(it.MonthlyManualCompetitiveConsumptionNumber))
}
// 可选:设置列宽,使表格更美观
_ = f.SetColWidth(sheet, "A", "AZ", 15)
_ = f.SetColWidth(sheet, "A", "BZ", 15)
// 保存文件
if err := f.SaveAs(filename); err != nil {
@ -609,6 +636,8 @@ func GetAccountBundleBalance(c *gin.Context) {
ImageConsumptionNumber: item.BundleImageConsumptionNumber + item.IncreaseImageConsumptionNumber + item.ManualImageNumber,
DataAnalysisNumber: item.BundleDataAnalysisNumber + item.IncreaseDataAnalysisNumber + item.ManualDataAnalysisNumber,
DataAnalysisConsumptionNumber: item.BundleDataAnalysisConsumptionNumber + item.IncreaseDataAnalysisConsumptionNumber + item.ManualDataAnalysisNumber,
CompetitiveNumber: item.BundleCompetitiveNumber + item.IncreaseCompetitiveNumber + item.ManualCompetitiveNumber,
CompetitiveConsumptionNumber: item.BundleCompetitiveConsumptionNumber + item.IncreaseCompetitiveConsumptionNumber + item.ManualCompetitiveConsumptionNumber,
Bought: item.Bought,
}
})

View File

@ -1,4 +1,4 @@
package cast
package cast
import (
"context"
@ -13,6 +13,7 @@ import (
modelCast "fonchain-fiee/pkg/model/cast"
"fonchain-fiee/pkg/model/login"
"fonchain-fiee/pkg/service"
"fonchain-fiee/pkg/service/bundle/common"
"fonchain-fiee/pkg/utils"
"strconv"
"sync"
@ -143,6 +144,41 @@ func ListWorkAnalysis(ctx *gin.Context) {
return
}
// ListWorkAnalysis 获取作品分析列表
func ListWorkAnalysisForApp(ctx *gin.Context) {
var req *cast.ListWorkAnalysisReq
var err error
if err = ctx.ShouldBind(&req); err != nil {
service.Error(ctx, err)
return
}
newCtx := NewCtxWithUserInfo(ctx)
loginInfo := login.GetUserInfoFromC(ctx)
// 查询用户套餐有没有过期
balanceInfoRes, err := service.BundleProvider.GetBundleBalanceByUserId(context.Background(), &bundle.GetBundleBalanceByUserIdReq{
UserId: int32(loginInfo.ID),
})
if err != nil {
zap.L().Error("ListWorkAnalysisForApp GetBundleBalanceByUserId", zap.Any("err", err))
service.Error(ctx, errors.New(common.GetUserBalanceFailed))
return
}
// 套餐未过期的话,传入 subNum ,只获取该套餐的有效期内数据
if balanceInfoRes.BundleStatus == common.BundleNotExpired {
zap.L().Info("ListWorkAnalysisForApp BundleNotExpired", zap.Any("loginInfo", loginInfo))
req.SubNum = loginInfo.SubNum
}
resp, err := service.CastProvider.ListWorkAnalysis(newCtx, req)
if err != nil {
zap.L().Error("ListWorkAnalysisForApp ListWorkAnalysis", zap.Any("err", err))
service.Error(ctx, err)
return
}
// RefreshWorkAnalysisApproval(ctx, resp.Data)
service.Success(ctx, resp)
return
}
// RefreshWorkAnalysisApproval 刷新作品分析审批状态
func RefreshWorkAnalysisApproval(ctx *gin.Context, data []*cast.WorkAnalysisInfo) {
if len(data) > 0 {
@ -538,6 +574,12 @@ func CheckBundleBalance(ctx *gin.Context) {
service.Error(ctx, err)
return
}
case modelCast.BalanceTypeCompetitiveValue:
if resp.CompetitiveExtendNumber-resp.CompetitiveExtendConsumptionNumber <= 0 {
err = errors.New(e.ErrorBalanceInsufficient)
service.Error(ctx, err)
return
}
}
service.Success(ctx, resp)
@ -603,6 +645,48 @@ func autoConfirmAnalysis(ctx context.Context, analysisUuid string) (err error) {
isFailed = true
}
if balanceInfoRes.BundleStatus == common.BundleExpired {
confirmRemark = "套餐已过期"
// 直接提交
_, err = service.CastProvider.UpdateWorkAnalysisStatus(context.Background(), &cast.UpdateWorkAnalysisStatusReq{
WorkAction: cast.WorkActionENUM_CONFIRM,
Uuid: analysisUuid,
ConfirmRemark: confirmRemark,
ConfirmStatus: 3,
})
if err != nil {
zap.L().Error("autoConfirmAnalysis UpdateWorkAnalysisStatus", zap.Any("err", err))
return
}
return
}
// 判断数据分析的提交时间是否在现在的套餐期间范围之内
submitTime, err := time.Parse("2006-01-02 15:04:05", infoResp.SubmitTime)
if err != nil {
zap.L().Error("autoConfirmAnalysis ParseSubmitTime", zap.Any("err", err))
return
}
if submitTime.Before(time.Unix(balanceInfoRes.PayTime, 0)) {
// todo 暂时先这样
// confirmRemark = "该报告提交时间不在该套餐期间范围之内"
// 直接提交
confirmRemark = "系统自动确认"
usedType = 0
_, err = service.CastProvider.UpdateWorkAnalysisStatus(context.Background(), &cast.UpdateWorkAnalysisStatusReq{
WorkAction: cast.WorkActionENUM_CONFIRM,
Uuid: analysisUuid,
ConfirmRemark: confirmRemark,
CostType: usedType,
ConfirmStatus: 1,
ConfirmType: 2,
})
if err != nil {
zap.L().Error("autoConfirmAnalysis UpdateWorkAnalysisStatus", zap.Any("err", err))
return
}
return
}
var addBalanceReq bundle.AddBundleBalanceReq
addBalanceReq.UserId = int32(userID)
// 检查数据分析余量

1131
pkg/service/cast/report.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -419,6 +419,12 @@ func CheckUserBundleBalance(userID int32, balanceType modelCast.BalanceTypeEnum)
// err = errors.New(e.ErrorBalanceInsufficient)
return
}
case modelCast.BalanceTypeCompetitiveValue:
if resp.CompetitiveExtendNumber-resp.CompetitiveExtendConsumptionNumber <= 0 {
err = errors.New("该艺人竞品数可用次数为0")
// err = errors.New(e.ErrorBalanceInsufficient)
return
}
}
return
}
@ -1799,3 +1805,24 @@ func checkAndReuploadImage(imageUrl string, mediaType string) (string, error) {
}
return compressUrl, nil
}
// WorkListPublished 获取已发布的作品列表
func WorkListPublished(ctx *gin.Context) {
var (
req *cast.WorkListPublishedReq
resp *cast.WorkListPublishedResp
)
var err error
if err = ctx.ShouldBind(&req); err != nil {
service.Error(ctx, err)
return
}
newCtx := NewCtxWithUserInfo(ctx)
resp, err = service.CastProvider.WorkListPublished(newCtx, req)
if err != nil {
service.Error(ctx, err)
return
}
service.Success(ctx, resp)
return
}

View File

@ -22,23 +22,6 @@ import (
"go.uber.org/zap"
)
// GetPendingTaskList 查询待指派任务记录
func GetPendingTaskList(c *gin.Context) {
var req bundle.TaskQueryRequest
if err := c.ShouldBindJSON(&req); err != nil {
service.Error(c, err)
return
}
res, err := service.BundleProvider.GetPendingTaskList(context.Background(), &req)
if err != nil {
service.Error(c, err)
return
}
service.Success(c, res)
}
func GetPendingTaskLayout(c *gin.Context) {
res, err := service.BundleProvider.GetPendingTaskLayout(context.Background(), &bundle.GetPendingTaskLayoutReq{})
if err != nil {
@ -144,26 +127,6 @@ func BatchTerminateTask(c *gin.Context) {
service.Success(c, res)
}
// UpdatePendingCount 修改待发数量
func UpdatePendingCount(c *gin.Context) {
var req bundle.UpdatePendingCountRequest
if err := c.ShouldBindJSON(&req); err != nil {
service.Error(c, err)
return
}
userInfo := login.GetUserInfoFromC(c)
req.OperatorNum = userInfo.TelNum
req.Operator = userInfo.Name
res, err := service.BundleProvider.UpdatePendingCount(context.Background(), &req)
if err != nil {
service.Error(c, err)
return
}
service.Success(c, res)
}
// GetRecentAssignRecords 查询最近被指派记录
func GetRecentAssignRecords(c *gin.Context) {
var req bundle.RecentAssignRecordsRequest
@ -238,23 +201,6 @@ func CompleteTaskManually(c *gin.Context) {
service.Success(c, res)
}
// UpdateTaskProgress 员工实际完成任务状态更新
func UpdateTaskProgress(c *gin.Context) {
var req bundle.UpdateTaskProgressRequest
if err := c.ShouldBindJSON(&req); err != nil {
service.Error(c, err)
return
}
res, err := service.BundleProvider.UpdateTaskProgress(context.Background(), &req)
if err != nil {
service.Error(c, err)
return
}
service.Success(c, res)
}
// GetTaskAssignRecordsList 多条件查询操作记录表
func GetTaskAssignRecordsList(c *gin.Context) {
var req bundle.TaskAssignRecordsQueryRequest
@ -532,23 +478,6 @@ func UpdateWorkVideoWithUUID(ctx *gin.Context) {
return
}
// GetArtistBundleBalance 查询艺人套餐剩余数量
func GetArtistBundleBalance(c *gin.Context) {
var req bundle.ArtistBundleBalanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
service.Error(c, err)
return
}
res, err := service.BundleProvider.GetArtistBundleBalance(context.Background(), &req)
if err != nil {
service.Error(c, err)
return
}
service.Success(c, res)
}
// GetArtistUploadStatsList 查询艺人待上传列表
func GetArtistUploadStatsList(c *gin.Context) {
var req bundle.TaskQueryRequest
@ -566,22 +495,6 @@ func GetArtistUploadStatsList(c *gin.Context) {
service.Success(c, res)
}
func GetPendingUploadBreakdown(c *gin.Context) {
var req bundle.PendingUploadBreakdownRequest
if err := c.ShouldBindJSON(&req); err != nil {
service.Error(c, err)
return
}
res, err := service.BundleProvider.GetPendingUploadBreakdown(context.Background(), &req)
if err != nil {
service.Error(c, err)
return
}
service.Success(c, res)
}
// GetArtistUploadStatsListDownload 导出艺人待上传列表为Excel
func GetArtistUploadStatsListDownload(c *gin.Context) {
var req bundle.TaskQueryRequest

View File

@ -7,6 +7,7 @@ import (
"io"
"net/http"
"os"
"strconv"
"strings"
"go.uber.org/zap"
@ -50,3 +51,50 @@ func SaveUrlFileDisk(url string, path string, filename string) (fullPath string,
}
return
}
// GetRemoteFileSize 通过HTTP HEAD请求获取远程文件大小不下载文件
func GetRemoteFileSize(url string) (size int64, err error) {
// 创建HEAD请求
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
zap.L().Error("GetRemoteFileSize create request err", zap.String("url", url), zap.Error(err))
err = errors.New(e.GetMsg(e.ERROR_DOWNLOAD_FILE))
return
}
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
zap.L().Error("GetRemoteFileSize request err", zap.String("url", url), zap.Error(err))
err = errors.New(e.GetMsg(e.ERROR_DOWNLOAD_FILE))
return
}
defer resp.Body.Close()
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
zap.L().Error("GetRemoteFileSize status code err", zap.String("url", url), zap.Int("status", resp.StatusCode))
err = errors.New(e.GetMsg(e.ERROR_DOWNLOAD_FILE))
return
}
// 获取Content-Length头部
contentLength := resp.Header.Get("Content-Length")
if contentLength == "" {
zap.L().Error("GetRemoteFileSize Content-Length header not found", zap.String("url", url))
err = errors.New("无法获取文件大小")
return
}
// 解析文件大小
size, err = strconv.ParseInt(contentLength, 10, 64)
size = size / 1024 / 1024
if err != nil {
zap.L().Error("GetRemoteFileSize parse size err", zap.String("url", url), zap.String("contentLength", contentLength), zap.Error(err))
err = errors.New("解析文件大小失败")
return
}
return
}

174
pkg/utils/pdf.go Normal file
View File

@ -0,0 +1,174 @@
package utils
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"unicode"
"github.com/phpdave11/gofpdf"
)
// cleanTextForPDF 清理文本移除PDF不支持的字符如emoji
// gofpdf库不支持某些特殊字符
func cleanTextForPDF(text string) string {
var result []rune
for _, r := range text {
// 保留基本多文种平面BMP内的字符码点 <= 0xFFFF
// 这样可以保留中文、英文、数字等常用字符但过滤掉emoji等特殊字符
if r <= 0xFFFF && (unicode.IsPrint(r) || unicode.IsSpace(r)) {
result = append(result, r)
}
}
return string(result)
}
// loadChineseFont 加载中文字体
func loadChineseFont(pdf *gofpdf.Fpdf, fontPath string) error {
var fontData []byte
var err error
// 如果提供了本地字体路径,优先使用本地字体
if fontPath == "" {
return errors.New("字体文件路径不能为空")
}
fontData, err = os.ReadFile(fontPath)
if err != nil {
return fmt.Errorf("读取字体文件失败: %v", err)
}
// 使用本地字体文件
pdf.AddUTF8FontFromBytes("Chinese", "", fontData)
return nil
}
// GeneratePDF 生成PDF文件
func GeneratePDF(text, imageURL, outputPath, fontPath string) error {
if text == "" {
return errors.New("文本不能为空")
}
// 创建PDF实例P=纵向mm=毫米单位A4=页面大小
pdf := gofpdf.New("P", "mm", "A4", "")
// 加载中文字体
err := loadChineseFont(pdf, fontPath)
if err != nil {
return fmt.Errorf("加载中文字体失败: %v", err)
}
// 添加新页面
pdf.AddPage()
// 设置字体使用中文字体12号字体
pdf.SetFont("Chinese", "", 12)
// 设置页面边距(左、上、右)
pdf.SetMargins(20, 10, 20)
// 设置当前位置x, y从左上角开始
pdf.SetXY(20, 10)
// 清理文本移除PDF不支持的字符如emoji
cleanedText := cleanTextForPDF(text)
// 添加文本内容
// 使用MultiCell方法处理多行文本支持自动换行
// 参数:宽度、行高、文本内容、边框、对齐方式、是否填充
// A4页面宽度210mm减去左右边距40mm可用宽度170mm
textWidth := 170.0
lineHeight := 7.0
pdf.MultiCell(textWidth, lineHeight, cleanedText, "", "L", false)
// 如果提供了图片URL则添加图片
if imageURL != "" {
// 添加一些间距
pdf.Ln(5)
// 解析URL获取文件扩展名
u, err := url.Parse(imageURL)
if err != nil {
return fmt.Errorf("图片链接解析错误: %v", err)
}
fileExt := filepath.Ext(u.Path)
// 如果没有扩展名,默认使用.jpg
if fileExt == "" {
fileExt = ".jpg"
}
// 下载图片
resp, err := http.Get(imageURL)
if err != nil {
return fmt.Errorf("下载图片失败: %v", err)
}
defer resp.Body.Close()
// 读取图片数据
imageData, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取图片数据失败: %v", err)
}
// 将图片数据保存到临时文件gofpdf需要文件路径
tmpFile, err := os.CreateTemp("", "pdf_image_*"+fileExt)
if err != nil {
return fmt.Errorf("创建临时文件失败: %v", err)
}
defer os.Remove(tmpFile.Name()) // 使用完后删除临时文件
defer tmpFile.Close()
// 写入图片数据到临时文件
_, err = tmpFile.Write(imageData)
if err != nil {
return fmt.Errorf("写入临时文件失败: %v", err)
}
tmpFile.Close()
// A4纵向页面宽度210mm减去左右边距40mm可用宽度170mm
// 图片宽度设为可用宽度的70%
imageWidth := textWidth * 0.7
// 计算居中位置页面宽度210mm图片居中
imageX := (210.0 - imageWidth) / 2
currentY := pdf.GetY()
// 注册图片并获取原始尺寸,用于计算缩放后的高度
imgInfo := pdf.RegisterImageOptions(tmpFile.Name(), gofpdf.ImageOptions{})
if imgInfo == nil {
return fmt.Errorf("注册图片失败")
}
// 计算缩放后的图片高度(按比例缩放)
// 原始宽度:原始高度 = 缩放后宽度:缩放后高度
originalWidth, originalHeight := imgInfo.Extent()
imageHeight := (imageWidth / originalWidth) * originalHeight
// A4页面高度297mm底部边距10mm计算可用的最大Y坐标
pageHeight := 297.0
bottomMargin := 10.0
maxY := pageHeight - bottomMargin
// 检查当前页面剩余空间是否足够放下图片
// 如果图片底部会超出页面可用区域,则添加新页面
if currentY+imageHeight > maxY {
pdf.AddPage()
// 新页面从顶部边距开始
currentY = 10.0
}
// 添加图片
// ImageOptions参数图片路径、x坐标、y坐标、宽度、高度、是否流式布局、选项、链接
// 高度设为0表示按比例自动计算
pdf.ImageOptions(tmpFile.Name(), imageX, currentY, imageWidth, 0, false, gofpdf.ImageOptions{}, 0, "")
}
// 生成并保存PDF文件
err = pdf.OutputFileAndClose(outputPath)
if err != nil {
return fmt.Errorf("生成PDF失败: %v", err)
}
return nil
}