feat: 优化报表系统功能与配置
This commit is contained in:
parent
04c67f86d0
commit
b22f4550ef
|
|
@ -29,6 +29,6 @@ func main() {
|
||||||
//钉钉机器人
|
//钉钉机器人
|
||||||
app.DingBotServer.Run(ctx, *onBot)
|
app.DingBotServer.Run(ctx, *onBot)
|
||||||
//定时任务
|
//定时任务
|
||||||
app.Cron.Run(ctx)
|
//app.Cron.Run(ctx)
|
||||||
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
|
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ server:
|
||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
|
|
||||||
ollama:
|
ollama:
|
||||||
base_url: "http://host.docker.internal:11434"
|
base_url: "http://127.0.0.1:11434"
|
||||||
model: "qwen3-coder:480b-cloud"
|
model: "qwen3-coder:480b-cloud"
|
||||||
generate_model: "qwen3-coder:480b-cloud"
|
generate_model: "qwen3-coder:480b-cloud"
|
||||||
mapping_model: "deepseek-v3.2:cloud"
|
mapping_model: "deepseek-v3.2:cloud"
|
||||||
|
|
@ -194,7 +194,3 @@ llm:
|
||||||
temperature: 0.7
|
temperature: 0.7
|
||||||
max_tokens: 4096
|
max_tokens: 4096
|
||||||
stream: true
|
stream: true
|
||||||
#ding_talk_bots:
|
|
||||||
# public:
|
|
||||||
# client_id: "dingchg59zwwvmuuvldx",
|
|
||||||
# client_secret: "ZwetAnRiTQobNFVlNrshRagSMAJIFpBAepWkWI7on7Tt_o617KHtTjBLp8fQfplz",
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import (
|
||||||
"ai_scheduler/internal/data/model"
|
"ai_scheduler/internal/data/model"
|
||||||
"ai_scheduler/internal/entitys"
|
"ai_scheduler/internal/entitys"
|
||||||
"ai_scheduler/internal/pkg/l_request"
|
"ai_scheduler/internal/pkg/l_request"
|
||||||
|
"ai_scheduler/internal/pkg/utils_oss"
|
||||||
"ai_scheduler/internal/tools"
|
"ai_scheduler/internal/tools"
|
||||||
|
"ai_scheduler/internal/tools/bbxt"
|
||||||
"ai_scheduler/tmpl/dataTemp"
|
"ai_scheduler/tmpl/dataTemp"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -44,6 +46,7 @@ type DingTalkBotBiz struct {
|
||||||
chatHis *impl.BotChatHisImpl
|
chatHis *impl.BotChatHisImpl
|
||||||
conf *config.Config
|
conf *config.Config
|
||||||
cardSend *dingtalk.SendCardClient
|
cardSend *dingtalk.SendCardClient
|
||||||
|
ossClient *utils_oss.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDingTalkBotBiz
|
// NewDingTalkBotBiz
|
||||||
|
|
@ -58,6 +61,7 @@ func NewDingTalkBotBiz(
|
||||||
toolManager *tools.Manager,
|
toolManager *tools.Manager,
|
||||||
conf *config.Config,
|
conf *config.Config,
|
||||||
cardSend *dingtalk.SendCardClient,
|
cardSend *dingtalk.SendCardClient,
|
||||||
|
ossClient *utils_oss.Client,
|
||||||
) *DingTalkBotBiz {
|
) *DingTalkBotBiz {
|
||||||
return &DingTalkBotBiz{
|
return &DingTalkBotBiz{
|
||||||
do: do,
|
do: do,
|
||||||
|
|
@ -71,6 +75,7 @@ func NewDingTalkBotBiz(
|
||||||
chatHis: chatHis,
|
chatHis: chatHis,
|
||||||
conf: conf,
|
conf: conf,
|
||||||
cardSend: cardSend,
|
cardSend: cardSend,
|
||||||
|
ossClient: ossClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -473,12 +478,34 @@ func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotC
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DingTalkBotBiz) GetReportLists(ctx context.Context) (contentChan chan string, err error) {
|
func (d *DingTalkBotBiz) GetReportLists(ctx context.Context) (reports []*bbxt.ReportRes, err error) {
|
||||||
contentChan = make(chan string, 10)
|
|
||||||
defer close(contentChan)
|
|
||||||
contentChan <- "截止今日23点利润亏损合计:127917.0866元,亏损500元以上的分销商和产品金额如下图:"
|
|
||||||
contentChan <- ""
|
|
||||||
|
|
||||||
|
reportList, err := bbxt.NewBbxtTools()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reports, err = reportList.DailyReport(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, d.ossClient)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo model.AiBotGroup, report *bbxt.ReportRes) (err error) {
|
||||||
|
|
||||||
|
reportChan := make(chan string, 10)
|
||||||
|
defer close(reportChan)
|
||||||
|
reportChan <- report.Title
|
||||||
|
reportChan <- fmt.Sprintf("", report.Url)
|
||||||
|
err = d.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
|
||||||
|
RobotCode: groupInfo.RobotCode,
|
||||||
|
ConversationType: constants.ConversationTypeGroup,
|
||||||
|
ConversationId: groupInfo.ConversationID,
|
||||||
|
Text: chatbot.BotCallbackDataTextModel{
|
||||||
|
Content: report.ReportName,
|
||||||
|
},
|
||||||
|
}, reportChan)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,7 @@ type Config struct {
|
||||||
DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
|
DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
|
||||||
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
|
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
|
||||||
LLM LLM `mapstructure:"llm"`
|
LLM LLM `mapstructure:"llm"`
|
||||||
// DingTalkBots map[string]*DingTalkBot `mapstructure:"ding_talk_bots"`
|
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
|
||||||
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SysPrompt struct {
|
type SysPrompt struct {
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func JsonStringIgonErr(data interface{}) string {
|
func JsonStringIgonErr(data interface{}) string {
|
||||||
|
|
@ -170,131 +168,257 @@ func SafeReplace(template string, replaceTag string, replacements ...string) (st
|
||||||
return template, nil
|
return template, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StructToMapUsingJsoniter(obj interface{}) (map[string]string, error) {
|
// 配置选项
|
||||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
type URLValuesOptions struct {
|
||||||
|
ArrayFormat string // 数组格式:"brackets" -> name[], "indices" -> name[0], "repeat" -> name=value1&name=value2
|
||||||
// 转换为JSON
|
TimeFormat string // 时间格式
|
||||||
jsonBytes, err := json.Marshal(obj)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析为map[string]interface{}
|
|
||||||
var tempMap map[string]interface{}
|
|
||||||
err = json.Unmarshal(jsonBytes, &tempMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为map[string]string
|
|
||||||
result := make(map[string]string)
|
|
||||||
for k, v := range tempMap {
|
|
||||||
result[k] = fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用结构体转 Query 参数
|
var defaultOptions = URLValuesOptions{
|
||||||
func StructToQuery(obj interface{}) (url.Values, error) {
|
ArrayFormat: "brackets", // 默认使用括号格式
|
||||||
values := url.Values{}
|
TimeFormat: time.DateTime,
|
||||||
v := reflect.ValueOf(obj)
|
}
|
||||||
t := reflect.TypeOf(obj)
|
|
||||||
|
|
||||||
// 如果是指针,获取指向的值
|
// StructToURLValues 将结构体转换为 url.Values
|
||||||
|
func StructToURLValues(input interface{}, options ...URLValuesOptions) (url.Values, error) {
|
||||||
|
opts := defaultOptions
|
||||||
|
if len(options) > 0 {
|
||||||
|
opts = options[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
values := url.Values{}
|
||||||
|
|
||||||
|
if input == nil {
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v := reflect.ValueOf(input)
|
||||||
|
t := reflect.TypeOf(input)
|
||||||
|
|
||||||
|
// 如果是指针,获取其指向的值
|
||||||
if v.Kind() == reflect.Ptr {
|
if v.Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
v = v.Elem()
|
v = v.Elem()
|
||||||
t = t.Elem()
|
t = t.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保是结构体
|
// 确保是结构体类型
|
||||||
if v.Kind() != reflect.Struct {
|
if v.Kind() != reflect.Struct {
|
||||||
return values, fmt.Errorf("expected struct, got %v", v.Kind())
|
return nil, fmt.Errorf("input must be a struct or pointer to struct")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 遍历结构体字段
|
||||||
for i := 0; i < v.NumField(); i++ {
|
for i := 0; i < v.NumField(); i++ {
|
||||||
field := v.Field(i)
|
field := t.Field(i)
|
||||||
fieldType := t.Field(i)
|
fieldValue := v.Field(i)
|
||||||
|
|
||||||
// 跳过零值字段(omitempty)
|
// 跳过非导出字段
|
||||||
tag := fieldType.Tag.Get("json")
|
if !field.IsExported() {
|
||||||
if strings.Contains(tag, "omitempty") && field.IsZero() {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取字段名
|
// 解析 JSON 标签(也可以支持 form 标签)
|
||||||
fieldName := getFieldName(fieldType)
|
tag := field.Tag.Get("json")
|
||||||
|
fieldName, omitempty := parseJSONTag(tag)
|
||||||
|
if fieldName == "-" {
|
||||||
|
continue // 忽略该字段
|
||||||
|
}
|
||||||
if fieldName == "" {
|
if fieldName == "" {
|
||||||
|
fieldName = field.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理指针类型
|
||||||
|
if fieldValue.Kind() == reflect.Ptr {
|
||||||
|
if fieldValue.IsNil() {
|
||||||
|
if omitempty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 可以为 nil 指针添加空值
|
||||||
|
values.Set(fieldName, "")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldValue = fieldValue.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理切片/数组
|
||||||
|
if fieldValue.Kind() == reflect.Slice || fieldValue.Kind() == reflect.Array {
|
||||||
|
if fieldValue.Len() == 0 && omitempty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将切片转换为 URL 参数
|
||||||
|
err := addSliceToValues(values, fieldName, fieldValue, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理不同类型的字段
|
// 检查是否需要忽略空值
|
||||||
addFieldToValues(values, fieldName, field)
|
if omitempty && isEmptyValue(fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换单个值
|
||||||
|
str, err := valueToString(fieldValue, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
values.Set(fieldName, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
return values, nil
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFieldName(field reflect.StructField) string {
|
// 解析 JSON 标签
|
||||||
tag := field.Tag.Get("json")
|
func parseJSONTag(tag string) (fieldName string, omitempty bool) {
|
||||||
if tag != "" {
|
if tag == "" {
|
||||||
parts := strings.Split(tag, ",")
|
return "", false
|
||||||
if parts[0] != "-" && parts[0] != "" {
|
|
||||||
return parts[0]
|
|
||||||
}
|
|
||||||
if parts[0] == "-" {
|
|
||||||
return "" // 跳过该字段
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return field.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func addFieldToValues(values url.Values, name string, field reflect.Value) {
|
|
||||||
if !field.IsValid() || field.IsZero() {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch field.Kind() {
|
parts := strings.Split(tag, ",")
|
||||||
case reflect.String:
|
fieldName = parts[0]
|
||||||
values.Add(name, field.String())
|
|
||||||
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
if len(parts) > 1 {
|
||||||
values.Add(name, strconv.FormatInt(field.Int(), 10))
|
for _, part := range parts[1:] {
|
||||||
|
if part == "omitempty" {
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
omitempty = true
|
||||||
values.Add(name, strconv.FormatUint(field.Uint(), 10))
|
|
||||||
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
values.Add(name, strconv.FormatFloat(field.Float(), 'f', -1, 64))
|
|
||||||
|
|
||||||
case reflect.Bool:
|
|
||||||
values.Add(name, strconv.FormatBool(field.Bool()))
|
|
||||||
|
|
||||||
case reflect.Slice:
|
|
||||||
// 处理切片,特别是 []string
|
|
||||||
if field.Type().Elem().Kind() == reflect.String {
|
|
||||||
for i := 0; i < field.Len(); i++ {
|
|
||||||
item := field.Index(i).String()
|
|
||||||
// 特殊处理 ct 字段
|
|
||||||
if name == "ct" {
|
|
||||||
formatted := strings.Replace(item, " ", "+", 1)
|
|
||||||
if i == 1 && field.Len() >= 2 {
|
|
||||||
formatted = formatted + ".999"
|
|
||||||
}
|
|
||||||
values.Add("ct[]", formatted)
|
|
||||||
} else {
|
|
||||||
values.Add(fmt.Sprintf("%s[]", name), item)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case reflect.Struct:
|
return fieldName, omitempty
|
||||||
// 处理 time.Time
|
}
|
||||||
if t, ok := field.Interface().(time.Time); ok {
|
|
||||||
values.Add(name, t.Format("2006-01-02+15:04:05"))
|
// 添加切片到 values
|
||||||
|
func addSliceToValues(values url.Values, fieldName string, slice reflect.Value, opts URLValuesOptions) error {
|
||||||
|
length := slice.Len()
|
||||||
|
if length == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch opts.ArrayFormat {
|
||||||
|
case "brackets":
|
||||||
|
// 格式:field[]=value1&field[]=value2
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
item := slice.Index(i)
|
||||||
|
str, err := valueToString(item, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values.Add(fieldName, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "indices":
|
||||||
|
// 格式:field[0]=value1&field[1]=value2
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
item := slice.Index(i)
|
||||||
|
str, err := valueToString(item, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values.Set(fmt.Sprintf("%s[%d]", fieldName, i), str)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "repeat":
|
||||||
|
// 格式:field=value1&field=value2
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
item := slice.Index(i)
|
||||||
|
str, err := valueToString(item, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values.Add(fieldName, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
values.Add(name, fmt.Sprintf("%v", field.Interface()))
|
// 默认使用 brackets 格式
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
item := slice.Index(i)
|
||||||
|
str, err := valueToString(item, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values.Add(fieldName+"[]", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将值转换为字符串
|
||||||
|
func valueToString(v reflect.Value, opts URLValuesOptions) (string, error) {
|
||||||
|
if !v.IsValid() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理不同类型
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return v.String(), nil
|
||||||
|
|
||||||
|
case reflect.Bool:
|
||||||
|
return strconv.FormatBool(v.Bool()), nil
|
||||||
|
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return strconv.FormatInt(v.Int(), 10), nil
|
||||||
|
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return strconv.FormatUint(v.Uint(), 10), nil
|
||||||
|
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
// 特殊处理 time.Time
|
||||||
|
if t, ok := v.Interface().(time.Time); ok {
|
||||||
|
return t.Format(opts.TimeFormat), nil
|
||||||
|
}
|
||||||
|
// 其他结构体递归处理
|
||||||
|
// 这里可以扩展为递归处理嵌套结构体
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 默认使用 fmt 的字符串表示
|
||||||
|
return fmt.Sprintf("%v", v.Interface()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%v", v.Interface()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查值是否为空
|
||||||
|
func isEmptyValue(v reflect.Value) bool {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return v.String() == ""
|
||||||
|
case reflect.Bool:
|
||||||
|
return false
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return v.Int() == 0
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return v.Uint() == 0
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return v.Float() == 0
|
||||||
|
case reflect.Slice, reflect.Array, reflect.Map:
|
||||||
|
return v.Len() == 0
|
||||||
|
case reflect.Ptr, reflect.Interface:
|
||||||
|
return v.IsNil()
|
||||||
|
case reflect.Struct:
|
||||||
|
if t, ok := v.Interface().(time.Time); ok {
|
||||||
|
return t.IsZero()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 方便函数:直接生成查询字符串
|
||||||
|
func StructToQueryString(input interface{}, options ...URLValuesOptions) (string, error) {
|
||||||
|
values, err := StructToURLValues(input, options...)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return values.Encode(), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/pkg/dingtalk"
|
"ai_scheduler/internal/pkg/dingtalk"
|
||||||
"ai_scheduler/internal/pkg/oss"
|
|
||||||
"ai_scheduler/internal/pkg/utils_langchain"
|
"ai_scheduler/internal/pkg/utils_langchain"
|
||||||
"ai_scheduler/internal/pkg/utils_ollama"
|
"ai_scheduler/internal/pkg/utils_ollama"
|
||||||
|
"ai_scheduler/internal/pkg/utils_oss"
|
||||||
"ai_scheduler/internal/pkg/utils_vllm"
|
"ai_scheduler/internal/pkg/utils_vllm"
|
||||||
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
|
|
@ -21,5 +21,5 @@ var ProviderSetClient = wire.NewSet(
|
||||||
dingtalk.NewContactClient,
|
dingtalk.NewContactClient,
|
||||||
dingtalk.NewNotableClient,
|
dingtalk.NewNotableClient,
|
||||||
|
|
||||||
oss.NewClient,
|
utils_oss.NewClient,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package oss
|
package utils_oss
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
|
|
@ -16,19 +16,19 @@ type Client struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient 初始化 OSS 客户端
|
// NewClient 初始化 OSS 客户端
|
||||||
func NewClient(cfg config.Oss) (*Client, error) {
|
func NewClient(cfg *config.Config) (*Client, error) {
|
||||||
client, err := oss.New(cfg.Endpoint, cfg.AccessKey, cfg.SecretKey)
|
client, err := oss.New(cfg.Oss.Endpoint, cfg.Oss.AccessKey, cfg.Oss.SecretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("oss new client failed: %v", err)
|
return nil, fmt.Errorf("oss new client failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket, err := client.Bucket(cfg.Bucket)
|
bucket, err := client.Bucket(cfg.Oss.Bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("oss get bucket failed: %v", err)
|
return nil, fmt.Errorf("oss get bucket failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
config: cfg,
|
config: cfg.Oss,
|
||||||
client: client,
|
client: client,
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
@ -3,10 +3,9 @@ package services
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/biz"
|
"ai_scheduler/internal/biz"
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
"ai_scheduler/internal/data/constants"
|
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
"github.com/gofiber/fiber/v2/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CronService struct {
|
type CronService struct {
|
||||||
|
|
@ -22,22 +21,23 @@ func NewCronService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) *
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *CronService) CronReportSend(ctx context.Context) error {
|
func (d *CronService) CronReportSend(ctx context.Context) error {
|
||||||
reportChan, err := d.dingTalkBotBiz.GetReportLists(ctx)
|
reports, err := d.dingTalkBotBiz.GetReportLists(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
groupId := 23
|
groupId := 28
|
||||||
groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId)
|
groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = d.dingTalkBotBiz.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
|
//contentChan <- "截止今日23点利润亏损合计:127917.0866元,亏损500元以上的分销商和产品金额如下图:"
|
||||||
RobotCode: groupInfo.RobotCode,
|
//contentChan <- ""
|
||||||
ConversationType: constants.ConversationTypeGroup,
|
for _, report := range reports {
|
||||||
ConversationId: groupInfo.ConversationID,
|
err = d.dingTalkBotBiz.SendReport(ctx, groupInfo, report)
|
||||||
Text: chatbot.BotCallbackDataTextModel{
|
if err != nil {
|
||||||
Content: "报表",
|
log.Error(err)
|
||||||
},
|
continue
|
||||||
}, reportChan)
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package services
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/biz"
|
"ai_scheduler/internal/biz"
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
"ai_scheduler/internal/data/constants"
|
|
||||||
"ai_scheduler/internal/entitys"
|
"ai_scheduler/internal/entitys"
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
|
@ -136,24 +135,3 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DingBotService) CronReportSend(ctx context.Context) error {
|
|
||||||
reportChan, err := d.dingTalkBotBiz.GetReportLists(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
groupId := 23
|
|
||||||
groupInfo, err := d.dingTalkBotBiz.GetGroupInfo(ctx, groupId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = d.dingTalkBotBiz.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
|
|
||||||
RobotCode: groupInfo.RobotCode,
|
|
||||||
ConversationType: constants.ConversationTypeGroup,
|
|
||||||
ConversationId: groupInfo.ConversationID,
|
|
||||||
Text: chatbot.BotCallbackDataTextModel{
|
|
||||||
Content: "报表",
|
|
||||||
},
|
|
||||||
}, reportChan)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"ai_scheduler/internal/pkg"
|
"ai_scheduler/internal/pkg"
|
||||||
"ai_scheduler/internal/pkg/dingtalk"
|
"ai_scheduler/internal/pkg/dingtalk"
|
||||||
"ai_scheduler/internal/pkg/utils_ollama"
|
"ai_scheduler/internal/pkg/utils_ollama"
|
||||||
|
"ai_scheduler/internal/pkg/utils_oss"
|
||||||
"ai_scheduler/internal/pkg/utils_vllm"
|
"ai_scheduler/internal/pkg/utils_vllm"
|
||||||
|
|
||||||
"ai_scheduler/internal/tools"
|
"ai_scheduler/internal/tools"
|
||||||
|
|
@ -27,7 +28,7 @@ import (
|
||||||
|
|
||||||
func Test_Report(t *testing.T) {
|
func Test_Report(t *testing.T) {
|
||||||
run()
|
run()
|
||||||
a := dingBotService.CronReportSend(context.Background())
|
a := cronService.CronReportSend(context.Background())
|
||||||
t.Log(a)
|
t.Log(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ var (
|
||||||
configConfig *config.Config
|
configConfig *config.Config
|
||||||
err error
|
err error
|
||||||
dingBotService *DingBotService
|
dingBotService *DingBotService
|
||||||
|
cronService *CronService
|
||||||
)
|
)
|
||||||
|
|
||||||
// run 函数是程序的入口函数,负责初始化和配置各个组件
|
// run 函数是程序的入口函数,负责初始化和配置各个组件
|
||||||
|
|
@ -99,7 +101,9 @@ func run() {
|
||||||
// 初始化处理器
|
// 初始化处理器
|
||||||
handle := do.NewHandle(ollamaService, manager, configConfig, sessionImpl, registry, oldClient, contactClient, notableClient)
|
handle := do.NewHandle(ollamaService, manager, configConfig, sessionImpl, registry, oldClient, contactClient, notableClient)
|
||||||
// 初始化钉钉机器人业务逻辑
|
// 初始化钉钉机器人业务逻辑
|
||||||
dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, toolRegis, botChatHisImpl, manager, configConfig, sendCardClient)
|
utils_ossClient, _ := utils_oss.NewClient(configConfig)
|
||||||
|
dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, toolRegis, botChatHisImpl, manager, configConfig, sendCardClient, utils_ossClient)
|
||||||
// 初始化钉钉机器人服务
|
// 初始化钉钉机器人服务
|
||||||
|
cronService = NewCronService(configConfig, dingTalkBotBiz)
|
||||||
dingBotService = NewDingBotService(configConfig, dingTalkBotBiz)
|
dingBotService = NewDingBotService(configConfig, dingTalkBotBiz)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -40,10 +41,10 @@ func StatisOursProductLossSumApi(param *StatisOursProductLossSumReq) (*StatisOur
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetProfitRankingSumRequest struct {
|
type GetProfitRankingSumRequest struct {
|
||||||
Ct []string `protobuf:"bytes,1,rep,name=ct,proto3" json:"ct,omitempty"`
|
Ct []string `json:"ct,omitempty"`
|
||||||
Page int32 `protobuf:"varint,3,opt,name=page,proto3" json:"page,omitempty"`
|
Page int32 `json:"page,omitempty"`
|
||||||
Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"`
|
Limit int32 `json:"limit,omitempty"`
|
||||||
ResellerIds []int32 `protobuf:"varint,5,rep,packed,name=reseller_ids,json=resellerIds,proto3" json:"reseller_ids,omitempty"`
|
ResellerIds []int32 `json:"reseller_ids,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetProfitRankingSumResponse struct {
|
type GetProfitRankingSumResponse struct {
|
||||||
|
|
@ -87,18 +88,18 @@ type GetStatisOfficialProductSumRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetStatisOfficialProductSumResponse struct {
|
type GetStatisOfficialProductSumResponse struct {
|
||||||
OfficialProductSum []*GetStatisOfficialProductSum `protobuf:"bytes,1,rep,name=official_product_sum,json=officialProductSum,proto3" json:"official_product_sum,omitempty"`
|
OfficialProductSum []*GetStatisOfficialProductSum `protobuf:"bytes,1,rep,name=OfficialProductSum,json=officialProductSum,proto3" json:"officialProductSum,omitempty"`
|
||||||
DataCount int32 `protobuf:"varint,2,opt,name=data_count,json=dataCount,proto3" json:"data_count,omitempty"`
|
DataCount int32 `protobuf:"varint,2,opt,name=DataCount,json=dataCount,proto3" json:"dataCount,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetStatisOfficialProductSum struct {
|
type GetStatisOfficialProductSum struct {
|
||||||
OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"official_product_id,omitempty"`
|
OfficialProductId int32 `protobuf:"varint,1,opt,name=official_product_id,json=officialProductId,proto3" json:"officialProductId,omitempty"`
|
||||||
OfficialProductName string `protobuf:"bytes,2,opt,name=official_product_name,json=officialProductName,proto3" json:"official_product_name,omitempty"`
|
OfficialProductName string `protobuf:"bytes,2,opt,name=official_product_name,json=officialProductName,proto3" json:"officialProductName,omitempty"`
|
||||||
CurrentNum int32 `protobuf:"varint,3,opt,name=current_num,json=currentNum,proto3" json:"current_num,omitempty"`
|
CurrentNum int32 `protobuf:"varint,3,opt,name=current_num,json=currentNum,proto3" json:"currentNum,omitempty"`
|
||||||
HistoryOneNum int32 `protobuf:"varint,4,opt,name=history_one_num,json=historyOneNum,proto3" json:"history_one_num,omitempty"`
|
HistoryOneNum int32 `protobuf:"varint,4,opt,name=history_one_num,json=historyOneNum,proto3" json:"historyOneNum,omitempty"`
|
||||||
HistoryTwoNum int32 `protobuf:"varint,5,opt,name=history_two_num,json=historyTwoNum,proto3" json:"history_two_num,omitempty"`
|
HistoryTwoNum int32 `protobuf:"varint,5,opt,name=history_two_num,json=historyTwoNum,proto3" json:"historyTwoNum,omitempty"`
|
||||||
HistoryOneDiff int32 `protobuf:"varint,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"history_one_diff,omitempty"`
|
HistoryOneDiff int32 `protobuf:"varint,6,opt,name=history_one_diff,json=historyOneDiff,proto3" json:"historyOneDiff,omitempty"`
|
||||||
HistoryTwoDiff int32 `protobuf:"varint,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"history_two_diff,omitempty"`
|
HistoryTwoDiff int32 `protobuf:"varint,7,opt,name=history_two_diff,json=historyTwoDiff,proto3" json:"historyTwoDiff,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStatisOfficialProductSumApi 销量同比分析
|
// GetStatisOfficialProductSumApi 销量同比分析
|
||||||
|
|
@ -169,16 +170,19 @@ func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequ
|
||||||
|
|
||||||
func request(url string, reqData interface{}, resData interface{}) error {
|
func request(url string, reqData interface{}, resData interface{}) error {
|
||||||
|
|
||||||
reqParam, err := pkg.StructToQuery(reqData)
|
reqParam, err := pkg.StructToURLValues(reqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &l_request.Request{
|
req := &l_request.Request{
|
||||||
Url: Base + url + "?" + customEncode(reqParam),
|
Url: FormatPHPURL(Base+url, reqParam),
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
}
|
}
|
||||||
res, err := req.Send()
|
res, err := req.Send()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("request failed, status code: %d,resion: %s", res.StatusCode, res.Reason)
|
return fmt.Errorf("request failed, status code: %d,resion: %s", res.StatusCode, res.Reason)
|
||||||
}
|
}
|
||||||
|
|
@ -195,13 +199,50 @@ func request(url string, reqData interface{}, resData interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func customEncode(params url.Values) string {
|
// FormatPHPURL 将 url.Values 格式化为 PHP 风格的 URL
|
||||||
encoded := params.Encode()
|
// 输入:基础URL和url.Values参数
|
||||||
|
// 输出:PHP风格的URL字符串
|
||||||
|
func FormatPHPURL(baseURL string, values url.Values) string {
|
||||||
|
if values == nil || len(values) == 0 {
|
||||||
|
return baseURL
|
||||||
|
}
|
||||||
|
|
||||||
// 解码我们想要保留的字符
|
var queryParts []string
|
||||||
encoded = strings.ReplaceAll(encoded, "%5B", "[") // 恢复 [
|
|
||||||
encoded = strings.ReplaceAll(encoded, "%5D", "]") // 恢复 ]
|
|
||||||
encoded = strings.ReplaceAll(encoded, "%2B", "+") // 恢复 +
|
|
||||||
|
|
||||||
return encoded
|
// 遍历所有参数
|
||||||
|
for key, paramValues := range values {
|
||||||
|
// 检查这个key是否有多个值(数组参数)
|
||||||
|
if len(paramValues) > 1 {
|
||||||
|
// 多值参数,使用PHP数组格式:key[]=value
|
||||||
|
for _, value := range paramValues {
|
||||||
|
if value != "" {
|
||||||
|
// 编码值
|
||||||
|
encodedValue := url.QueryEscape(value)
|
||||||
|
// 使用PHP数组格式
|
||||||
|
queryParts = append(queryParts, fmt.Sprintf("%s[]=%s", key, encodedValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if len(paramValues) == 1 && paramValues[0] != "" {
|
||||||
|
// 单值参数
|
||||||
|
encodedValue := url.QueryEscape(paramValues[0])
|
||||||
|
queryParts = append(queryParts, fmt.Sprintf("%s=%s", key, encodedValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(queryParts) == 0 {
|
||||||
|
return baseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建查询字符串
|
||||||
|
query := strings.Join(queryParts, "&")
|
||||||
|
|
||||||
|
// 转换为PHP风格:解码中括号和冒号
|
||||||
|
query = strings.ReplaceAll(query, "%5B", "[")
|
||||||
|
query = strings.ReplaceAll(query, "%5D", "]")
|
||||||
|
query = strings.ReplaceAll(query, "%3A", ":")
|
||||||
|
|
||||||
|
// 注意:保留空格为+号(这是PHP的常见格式)
|
||||||
|
// query = strings.ReplaceAll(query, "+", "%20") // 如果需要将+转为%20,可以取消注释
|
||||||
|
|
||||||
|
return baseURL + "?" + query
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,28 @@
|
||||||
package bbxt
|
package bbxt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/pkg/oss"
|
"ai_scheduler/internal/pkg/utils_oss"
|
||||||
"ai_scheduler/pkg"
|
"ai_scheduler/pkg"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-kratos/kratos/v2/log"
|
"sort"
|
||||||
"github.com/xuri/excelize/v2"
|
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RedStyle = "${color: FF0000;horizontal:center;vertical:center;borderColor:#000000}"
|
||||||
|
GreenStyle = "${color: 00B050;horizontal:center;vertical:center;borderColor:#000000}"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BbxtTools struct {
|
type BbxtTools struct {
|
||||||
cacheDir string
|
cacheDir string
|
||||||
excelTempDir string
|
excelTempDir string
|
||||||
ossClient *oss.Client
|
ossClient *utils_oss.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBbxtTools(ossClient *oss.Client) (*BbxtTools, error) {
|
func NewBbxtTools() (*BbxtTools, error) {
|
||||||
cache, err := pkg.GetCacheDir()
|
cache, err := pkg.GetCacheDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -34,24 +35,42 @@ func NewBbxtTools(ossClient *oss.Client) (*BbxtTools, error) {
|
||||||
return &BbxtTools{
|
return &BbxtTools{
|
||||||
cacheDir: cache,
|
cacheDir: cache,
|
||||||
excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir),
|
excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir),
|
||||||
ossClient: ossClient,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BbxtTools) DailyReport(now time.Time) (err error) {
|
func (b *BbxtTools) DailyReport(now time.Time, productName []string, ossClient *utils_oss.Client) (reports []*ReportRes, err error) {
|
||||||
|
reports = make([]*ReportRes, 0, 4)
|
||||||
err = b.StatisOursProductLossSum([]string{
|
productLossReport, err := b.StatisOursProductLossSum(now)
|
||||||
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
|
||||||
time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
profitRankingSum, err := b.GetProfitRankingSum(now)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statisOfficialProductSum, err := b.GetStatisOfficialProductSum(now, productName)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reports = append(reports, productLossReport...)
|
||||||
|
reports = append(reports, statisOfficialProductSum, profitRankingSum)
|
||||||
|
uploader := NewUploader(ossClient)
|
||||||
|
if ossClient != nil {
|
||||||
|
for _, report := range reports {
|
||||||
|
_ = uploader.Run(report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatisOursProductLossSumTotal 负利润分析
|
// StatisOursProductLossSum 负利润分析
|
||||||
func (b *BbxtTools) StatisOursProductLossSum(ct []string) (err error) {
|
func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes, err error) {
|
||||||
|
ct := []string{
|
||||||
|
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
||||||
|
adjustedTime(time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())),
|
||||||
|
}
|
||||||
|
|
||||||
data, err := StatisOursProductLossSumApi(&StatisOursProductLossSumReq{
|
data, err := StatisOursProductLossSumApi(&StatisOursProductLossSumReq{
|
||||||
Ct: ct,
|
Ct: ct,
|
||||||
})
|
})
|
||||||
|
|
@ -106,7 +125,10 @@ func (b *BbxtTools) StatisOursProductLossSum(ct []string) (err error) {
|
||||||
sort.Slice(resellers, func(i, j int) bool {
|
sort.Slice(resellers, func(i, j int) bool {
|
||||||
return resellers[i].Total < resellers[j].Total
|
return resellers[i].Total < resellers[j].Total
|
||||||
})
|
})
|
||||||
|
var (
|
||||||
|
totalSum float64
|
||||||
|
totalSum500 float64
|
||||||
|
)
|
||||||
// 构建分组
|
// 构建分组
|
||||||
for _, v := range resellers {
|
for _, v := range resellers {
|
||||||
if v.Total <= -100 {
|
if v.Total <= -100 {
|
||||||
|
|
@ -117,27 +139,48 @@ func (b *BbxtTools) StatisOursProductLossSum(ct []string) (err error) {
|
||||||
}
|
}
|
||||||
if v.Total <= -500 {
|
if v.Total <= -500 {
|
||||||
gt = append(gt, v)
|
gt = append(gt, v)
|
||||||
|
totalSum500 += v.Total
|
||||||
}
|
}
|
||||||
|
totalSum += v.Total
|
||||||
}
|
}
|
||||||
|
report = make([]*ReportRes, 2)
|
||||||
|
|
||||||
//总量生成excel
|
//总量生成excel
|
||||||
if len(total) > 0 {
|
if len(total) > 0 {
|
||||||
filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
||||||
err = b.SimpleFillExcel(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total)
|
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total, "")
|
||||||
|
report[0] = &ReportRes{
|
||||||
|
ReportName: "负利润分析(合计表)",
|
||||||
|
Title: "截至今日23点利润累计亏损" + fmt.Sprintf("%.2f", totalSum),
|
||||||
|
Path: filePath,
|
||||||
|
Data: total,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(gt) > 0 {
|
if len(gt) > 0 {
|
||||||
filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
|
||||||
// err = b.resellerDetailFillExcel(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt)
|
|
||||||
err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt)
|
err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt)
|
||||||
|
report[1] = &ReportRes{
|
||||||
|
ReportName: "负利润分析(亏损500以上)",
|
||||||
|
Title: "截至今日23点亏顺500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500),
|
||||||
|
Path: filePath,
|
||||||
|
Data: total,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return err
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProfitRankingSum 利润同比分销商排行榜
|
// GetProfitRankingSum 利润同比分销商排行榜
|
||||||
func (b *BbxtTools) GetProfitRankingSum(now time.Time) (err error) {
|
func (b *BbxtTools) GetProfitRankingSum(now time.Time) (report *ReportRes, err error) {
|
||||||
|
|
||||||
ct := []string{
|
ct := []string{
|
||||||
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
||||||
now.Format(time.DateTime),
|
adjustedTime(now),
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := GetProfitRankingSumApi(&GetProfitRankingSumRequest{
|
data, err := GetProfitRankingSumApi(&GetProfitRankingSumRequest{
|
||||||
|
|
@ -166,9 +209,9 @@ func (b *BbxtTools) GetProfitRankingSum(now time.Time) (err error) {
|
||||||
for _, v := range top {
|
for _, v := range top {
|
||||||
var diff string
|
var diff string
|
||||||
if v.HistoryOneDiff > 0 {
|
if v.HistoryOneDiff > 0 {
|
||||||
diff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%.4f", v.HistoryOneDiff)
|
diff = fmt.Sprintf("%s↑%.4f", RedStyle, v.HistoryOneDiff)
|
||||||
} else {
|
} else {
|
||||||
diff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%.4f", v.HistoryOneDiff)
|
diff = fmt.Sprintf("%s↓%.4f", GreenStyle, v.HistoryOneDiff)
|
||||||
}
|
}
|
||||||
total = append(total, []string{
|
total = append(total, []string{
|
||||||
fmt.Sprintf("%s", v.ResellerName),
|
fmt.Sprintf("%s", v.ResellerName),
|
||||||
|
|
@ -178,18 +221,25 @@ func (b *BbxtTools) GetProfitRankingSum(now time.Time) (err error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
//总量生成excel
|
//总量生成excel
|
||||||
if len(total) > 0 {
|
if len(total) == 0 {
|
||||||
filePath := b.cacheDir + "/lrtb_rank" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx"
|
return
|
||||||
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"lrtb_rank.xlsx", filePath, total, title)
|
|
||||||
}
|
}
|
||||||
return err
|
filePath := b.cacheDir + "/lrtb_rank" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx"
|
||||||
|
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"lrtb_rank.xlsx", filePath, total, title)
|
||||||
|
return &ReportRes{
|
||||||
|
ReportName: "利润同比分销商排行榜",
|
||||||
|
Title: title,
|
||||||
|
Path: filePath,
|
||||||
|
Data: total,
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStatisOfficialProductSum 利润同比分销商排行榜
|
// GetStatisOfficialProductSum 利润同比分销商排行榜
|
||||||
func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []string) (err error) {
|
func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []string) (report *ReportRes, err error) {
|
||||||
|
|
||||||
ct := []string{
|
ct := []string{
|
||||||
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Format("2006-01-02 15:04:05"),
|
||||||
now.Format(time.DateTime),
|
adjustedTime(now),
|
||||||
}
|
}
|
||||||
var ids []int32
|
var ids []int32
|
||||||
if len(productName) > 0 {
|
if len(productName) > 0 {
|
||||||
|
|
@ -215,14 +265,14 @@ func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []str
|
||||||
lastWeekDiff string
|
lastWeekDiff string
|
||||||
)
|
)
|
||||||
if v.HistoryOneDiff > 0 {
|
if v.HistoryOneDiff > 0 {
|
||||||
yeterDatyDiff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryOneDiff)
|
yeterDatyDiff = fmt.Sprintf("%s↑%d", RedStyle, v.HistoryOneDiff)
|
||||||
} else {
|
} else {
|
||||||
yeterDatyDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", v.HistoryOneDiff)
|
yeterDatyDiff = fmt.Sprintf("%s↓%d", GreenStyle, v.HistoryOneDiff)
|
||||||
}
|
}
|
||||||
if v.HistoryTwoDiff > 0 {
|
if v.HistoryTwoDiff > 0 {
|
||||||
lastWeekDiff = fmt.Sprintf("${color: FF0000;horizontal:center;vertical:center}↑%d", v.HistoryTwoDiff)
|
lastWeekDiff = fmt.Sprintf("%s↑%d", RedStyle, v.HistoryTwoDiff)
|
||||||
} else {
|
} else {
|
||||||
lastWeekDiff = fmt.Sprintf("${color: 00B050;horizontal:center;vertical:center}↓%d", v.HistoryTwoDiff)
|
lastWeekDiff = fmt.Sprintf("%s↓%d", GreenStyle, v.HistoryTwoDiff)
|
||||||
}
|
}
|
||||||
total = append(total, []string{
|
total = append(total, []string{
|
||||||
fmt.Sprintf("%s", v.OfficialProductName),
|
fmt.Sprintf("%s", v.OfficialProductName),
|
||||||
|
|
@ -237,11 +287,17 @@ func (b *BbxtTools) GetStatisOfficialProductSum(now time.Time, productName []str
|
||||||
timeCh := now.Format("1月2日15点")
|
timeCh := now.Format("1月2日15点")
|
||||||
title := "截至" + timeCh + "销售同比分析"
|
title := "截至" + timeCh + "销售同比分析"
|
||||||
//总量生成excel
|
//总量生成excel
|
||||||
if len(total) > 0 {
|
if len(total) == 0 {
|
||||||
filePath := b.cacheDir + "/xstb_ana" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx"
|
return
|
||||||
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"xstb_ana.xlsx", filePath, total, title)
|
|
||||||
}
|
}
|
||||||
return err
|
filePath := b.cacheDir + "/xstb_ana" + fmt.Sprintf("%d", time.Now().Unix()) + ".xlsx"
|
||||||
|
err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"xstb_ana.xlsx", filePath, total, title)
|
||||||
|
return &ReportRes{
|
||||||
|
ReportName: "利润同比分销商排行榜",
|
||||||
|
Title: title,
|
||||||
|
Path: filePath,
|
||||||
|
Data: total,
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BbxtTools) getProductIdFromProductName(productNames []string) ([]int32, error) {
|
func (b *BbxtTools) getProductIdFromProductName(productNames []string) ([]int32, error) {
|
||||||
|
|
@ -262,149 +318,11 @@ func (b *BbxtTools) getProductIdFromProductName(productNames []string) ([]int32,
|
||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BbxtTools) SimpleFillExcelWithTitle(templatePath, outputPath string, dataSlice interface{}, title string) error {
|
func adjustedTime(t time.Time) string {
|
||||||
// 1. 打开模板
|
adjusted := time.Date(
|
||||||
f, err := excelize.OpenFile(templatePath)
|
t.Year(), t.Month(), t.Day(),
|
||||||
if err != nil {
|
t.Hour(), t.Minute(), 59, 999_000_000,
|
||||||
return err
|
t.Location(),
|
||||||
}
|
)
|
||||||
defer f.Close()
|
return adjusted.Format("2006-01-02 15:04:05.999")
|
||||||
|
|
||||||
sheet := f.GetSheetName(0)
|
|
||||||
|
|
||||||
// 1.1 获取第三行模板样式
|
|
||||||
templateRow := 3
|
|
||||||
styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", templateRow))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("获取模板样式失败: %v", err)
|
|
||||||
styleID = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.2 获取模板行高
|
|
||||||
rowHeight, err := f.GetRowHeight(sheet, templateRow)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("获取模板行高失败: %v", err)
|
|
||||||
rowHeight = 31 // 默认高度
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 写入标题到第一行
|
|
||||||
f.SetCellValue(sheet, "A1", title)
|
|
||||||
|
|
||||||
// 3. 反射获取切片数据
|
|
||||||
v := reflect.ValueOf(dataSlice)
|
|
||||||
if v.Kind() != reflect.Slice {
|
|
||||||
return fmt.Errorf("dataSlice must be a slice")
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.Len() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 从第三行开始填充数据(第二行留空或作为标题行)
|
|
||||||
startRow := 3
|
|
||||||
pattern := `\$\{(.*?)\}`
|
|
||||||
re := regexp.MustCompile(pattern)
|
|
||||||
for i := 0; i < v.Len(); i++ {
|
|
||||||
currentRow := startRow + i
|
|
||||||
|
|
||||||
// 获取当前行数据
|
|
||||||
item := v.Index(i)
|
|
||||||
|
|
||||||
// 处理不同类型的切片
|
|
||||||
var rowData []interface{}
|
|
||||||
|
|
||||||
if item.Kind() == reflect.Slice || item.Kind() == reflect.Array {
|
|
||||||
// 处理 []string 或 [][]string 中的一行
|
|
||||||
for j := 0; j < item.Len(); j++ {
|
|
||||||
if item.Index(j).CanInterface() {
|
|
||||||
rowData = append(rowData, item.Index(j).Interface())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if item.Kind() == reflect.Interface {
|
|
||||||
// 处理 interface{} 类型
|
|
||||||
if actualValue, ok := item.Interface().([]string); ok {
|
|
||||||
for _, val := range actualValue {
|
|
||||||
rowData = append(rowData, val)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rowData = []interface{}{item.Interface()}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rowData = []interface{}{item.Interface()}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4.1 设置行高
|
|
||||||
f.SetRowHeight(sheet, currentRow, rowHeight)
|
|
||||||
|
|
||||||
// 5. 填充数据到Excel
|
|
||||||
for col, value := range rowData {
|
|
||||||
cell := fmt.Sprintf("%c%d", 'A'+col, currentRow)
|
|
||||||
// 5.1 应用模板样式到整行(根据实际列数)
|
|
||||||
if styleID != 0 && len(rowData) > 0 {
|
|
||||||
startCol := "A"
|
|
||||||
endCol := fmt.Sprintf("%c", 'A'+len(rowData)-1)
|
|
||||||
endCell := fmt.Sprintf("%s%d", endCol, currentRow)
|
|
||||||
|
|
||||||
f.SetCellStyle(sheet, fmt.Sprintf("%s%d", startCol, currentRow),
|
|
||||||
endCell, styleID)
|
|
||||||
}
|
|
||||||
switch value.(type) {
|
|
||||||
case string:
|
|
||||||
var style = value.(string)
|
|
||||||
if re.MatchString(style) {
|
|
||||||
matches := re.FindStringSubmatch(style)
|
|
||||||
styleMap := make(map[string]string)
|
|
||||||
//matches = strings.Replace(matches, "$", "", 1)
|
|
||||||
if len(matches) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, kv := range strings.Split(matches[1], ";") {
|
|
||||||
kvParts := strings.Split(kv, ":")
|
|
||||||
if len(kvParts) == 2 {
|
|
||||||
styleMap[strings.TrimSpace(kvParts[0])] = strings.TrimSpace(kvParts[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fontStyleID, _err := SetStyle(styleMap, f)
|
|
||||||
if _err == nil {
|
|
||||||
f.SetCellStyle(sheet, cell, cell, fontStyleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
value = re.ReplaceAllString(style, "")
|
|
||||||
|
|
||||||
}
|
|
||||||
f.SetCellValue(sheet, cell, value)
|
|
||||||
default:
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 保存
|
|
||||||
return f.SaveAs(outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetStyle(styleMap map[string]string, f *excelize.File) (int, error) {
|
|
||||||
|
|
||||||
var style = &excelize.Style{}
|
|
||||||
if colorHex, exists := styleMap["color"]; exists {
|
|
||||||
style.Font = &excelize.Font{
|
|
||||||
Color: colorHex,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if horizontal, exists := styleMap["horizontal"]; exists {
|
|
||||||
if style.Alignment == nil {
|
|
||||||
style.Alignment = &excelize.Alignment{}
|
|
||||||
}
|
|
||||||
style.Alignment.Horizontal = horizontal
|
|
||||||
}
|
|
||||||
|
|
||||||
if vertical, exists := styleMap["vertical"]; exists {
|
|
||||||
if style.Alignment == nil {
|
|
||||||
style.Alignment = &excelize.Alignment{}
|
|
||||||
}
|
|
||||||
style.Alignment.Vertical = vertical
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.NewStyle(style)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,30 +2,44 @@ package bbxt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
"ai_scheduler/internal/pkg/oss"
|
"ai_scheduler/internal/pkg/utils_oss"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
|
func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
|
||||||
ossClient, err := oss.NewClient(config.Oss{
|
var config = &config.Config{
|
||||||
AccessKey: "LTAI5tGGZzjf3tvqWk8SQj2G",
|
Oss: config.Oss{
|
||||||
SecretKey: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq",
|
AccessKey: "LTAI5tGGZzjf3tvqWk8SQj2G",
|
||||||
Bucket: "attachment-public",
|
SecretKey: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq",
|
||||||
Domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com",
|
Bucket: "attachment-public",
|
||||||
Endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
|
Domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com",
|
||||||
})
|
Endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ossClient, err := utils_oss.NewClient(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
o, err := NewBbxtTools()
|
||||||
o, err := NewBbxtTools(ossClient)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
err = o.DailyReport(time.Date(2025, 12, 30, 0, 0, 0, 0, time.Local))
|
reports, err := o.DailyReport(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, ossClient)
|
||||||
|
|
||||||
t.Log(err)
|
t.Log(reports, err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_StatisOursProductLossSum(t *testing.T) {
|
||||||
|
o, err := NewBbxtTools()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
report, err := o.StatisOursProductLossSum(time.Now())
|
||||||
|
|
||||||
|
t.Log(report, err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,9 +48,9 @@ func Test_GetProfitRankingSum(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
err = o.GetProfitRankingSum(time.Now())
|
report, err := o.GetProfitRankingSum(time.Now())
|
||||||
|
|
||||||
t.Log(err)
|
t.Log(report, err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,8 +59,8 @@ func Test_GetStatisOfficialProductSum(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
err = o.GetStatisOfficialProductSum(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"})
|
report, err := o.GetStatisOfficialProductSum(time.Now(), []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"})
|
||||||
|
|
||||||
t.Log(err)
|
t.Log(report, err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,12 @@ type ProductLoss struct {
|
||||||
ProductName string
|
ProductName string
|
||||||
Loss float64
|
Loss float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReportRes struct {
|
||||||
|
ReportName string
|
||||||
|
Title string
|
||||||
|
Path string
|
||||||
|
Url string
|
||||||
|
Data [][]string
|
||||||
|
Desc string
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
package bbxt
|
package bbxt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -17,9 +12,9 @@ import (
|
||||||
"github.com/xuri/excelize/v2"
|
"github.com/xuri/excelize/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 最简单的通用函数
|
func (b *BbxtTools) SimpleFillExcelWithTitle(templatePath, outputPath string, dataSlice interface{}, title string) error {
|
||||||
func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice interface{}) error {
|
// 打开模板
|
||||||
// 1. 打开模板
|
|
||||||
f, err := excelize.OpenFile(templatePath)
|
f, err := excelize.OpenFile(templatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -27,175 +22,155 @@ func (b *BbxtTools) SimpleFillExcel(templatePath, outputPath string, dataSlice i
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
sheet := f.GetSheetName(0)
|
sheet := f.GetSheetName(0)
|
||||||
|
startLen := 2
|
||||||
// 1.1 获取第二行模板样式
|
if len(title) > 0 {
|
||||||
resellerTplRow := 2
|
// 写入标题
|
||||||
styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow))
|
f.SetCellValue(sheet, "A1", title)
|
||||||
if err != nil {
|
startLen = 3
|
||||||
log.Errorf("获取分销商总计样式失败: %v", err)
|
|
||||||
styleIDReseller = 0
|
|
||||||
}
|
}
|
||||||
// 1.2 获取分销商总计行高
|
// 获取模板样式
|
||||||
rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow)
|
templateRow := startLen
|
||||||
|
styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", templateRow))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("获取分销商总计行高失败: %v", err)
|
log.Errorf("获取模板样式失败: %v", err)
|
||||||
rowHeightReseller = 31 // 默认高度
|
styleID = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 反射获取切片数据
|
// 获取模板行高
|
||||||
|
rowHeight, err := f.GetRowHeight(sheet, templateRow)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("获取模板行高失败: %v", err)
|
||||||
|
rowHeight = 31 // 默认高度
|
||||||
|
}
|
||||||
|
|
||||||
|
// 反射获取切片数据
|
||||||
v := reflect.ValueOf(dataSlice)
|
v := reflect.ValueOf(dataSlice)
|
||||||
if v.Kind() != reflect.Slice {
|
if v.Kind() != reflect.Slice {
|
||||||
return fmt.Errorf("dataSlice must be a slice")
|
return fmt.Errorf("dataSlice must be a slice")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 从第2行开始填充
|
if v.Len() == 0 {
|
||||||
row := 2
|
return nil
|
||||||
for i := 0; i < v.Len(); i++ {
|
}
|
||||||
item := v.Index(i).Interface()
|
|
||||||
currentRow := row + i
|
|
||||||
|
|
||||||
// 4. 将item转换为一行数据
|
// 从第三行开始填充数据(第二行留空或作为标题行)
|
||||||
|
startRow := startLen
|
||||||
|
pattern := `\$\{(.*?)\}`
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
currentRow := startRow + i
|
||||||
|
|
||||||
|
// 获取当前行数据
|
||||||
|
item := v.Index(i)
|
||||||
|
|
||||||
|
// 处理不同类型的切片
|
||||||
var rowData []interface{}
|
var rowData []interface{}
|
||||||
|
|
||||||
// 如果是切片
|
if item.Kind() == reflect.Slice || item.Kind() == reflect.Array {
|
||||||
if reflect.TypeOf(item).Kind() == reflect.Slice {
|
// 处理 []string 或 [][]string 中的一行
|
||||||
itemV := reflect.ValueOf(item)
|
for j := 0; j < item.Len(); j++ {
|
||||||
for j := 0; j < itemV.Len(); j++ {
|
if item.Index(j).CanInterface() {
|
||||||
rowData = append(rowData, itemV.Index(j).Interface())
|
rowData = append(rowData, item.Index(j).Interface())
|
||||||
}
|
|
||||||
} else if reflect.TypeOf(item).Kind() == reflect.Struct {
|
|
||||||
itemV := reflect.ValueOf(item)
|
|
||||||
for j := 0; j < itemV.NumField(); j++ {
|
|
||||||
if itemV.Field(j).CanInterface() {
|
|
||||||
rowData = append(rowData, itemV.Field(j).Interface())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if item.Kind() == reflect.Interface {
|
||||||
|
// 处理 interface{} 类型
|
||||||
|
if actualValue, ok := item.Interface().([]string); ok {
|
||||||
|
for _, val := range actualValue {
|
||||||
|
rowData = append(rowData, val)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rowData = []interface{}{item.Interface()}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
rowData = []interface{}{item}
|
rowData = []interface{}{item.Interface()}
|
||||||
}
|
}
|
||||||
// 4.1 设置行高
|
|
||||||
f.SetRowHeight(sheet, currentRow, rowHeightReseller)
|
|
||||||
|
|
||||||
// 5. 填充到Excel
|
// 4.1 设置行高
|
||||||
|
f.SetRowHeight(sheet, currentRow, rowHeight)
|
||||||
|
|
||||||
|
// 应用模板样式到整行(根据实际列数)
|
||||||
|
if styleID != 0 && len(rowData) > 0 {
|
||||||
|
startCol := "A"
|
||||||
|
endCol := fmt.Sprintf("%c", 'A'+len(rowData)-1)
|
||||||
|
endCell := fmt.Sprintf("%s%d", endCol, currentRow)
|
||||||
|
|
||||||
|
f.SetCellStyle(sheet, fmt.Sprintf("%s%d", startCol, currentRow),
|
||||||
|
endCell, styleID)
|
||||||
|
}
|
||||||
|
// 填充数据到Excel
|
||||||
for col, value := range rowData {
|
for col, value := range rowData {
|
||||||
cell := fmt.Sprintf("%c%d", 'A'+col, currentRow)
|
cell := fmt.Sprintf("%c%d", 'A'+col, currentRow)
|
||||||
f.SetCellValue(sheet, cell, value)
|
|
||||||
|
switch value.(type) {
|
||||||
|
case string:
|
||||||
|
var style = value.(string)
|
||||||
|
if re.MatchString(style) {
|
||||||
|
matches := re.FindStringSubmatch(style)
|
||||||
|
styleMap := make(map[string]string)
|
||||||
|
//matches = strings.Replace(matches, "$", "", 1)
|
||||||
|
if len(matches) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, kv := range strings.Split(matches[1], ";") {
|
||||||
|
kvParts := strings.Split(kv, ":")
|
||||||
|
if len(kvParts) == 2 {
|
||||||
|
styleMap[strings.TrimSpace(kvParts[0])] = strings.TrimSpace(kvParts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fontStyleID, _err := SetStyle(styleMap, f)
|
||||||
|
if _err == nil {
|
||||||
|
f.SetCellStyle(sheet, cell, cell, fontStyleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
value = re.ReplaceAllString(style, "")
|
||||||
|
|
||||||
|
}
|
||||||
|
f.SetCellValue(sheet, cell, value)
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5.1 使用第二行模板样式
|
|
||||||
if styleIDReseller != 0 {
|
|
||||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 保存
|
||||||
excelBytes, err := f.WriteToBuffer()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("write to bytes failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
picBytes, err := b.excel2picPy(templatePath, excelBytes.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("excel2picPy failed: %v", err)
|
|
||||||
}
|
|
||||||
// b.savePic("temp.png", picBytes) // 本地生成图片,仅测试
|
|
||||||
// outputPath 提取文件名(不包含扩展名)
|
|
||||||
filename := filepath.Base(outputPath)
|
|
||||||
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
|
||||||
imgUrl := b.uploadToOSS(filename, picBytes)
|
|
||||||
log.Infof("imgUrl: %s", imgUrl)
|
|
||||||
|
|
||||||
// 6. 保存
|
|
||||||
return f.SaveAs(outputPath)
|
return f.SaveAs(outputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分销商负利润详情填充excel
|
func SetStyle(styleMap map[string]string, f *excelize.File) (int, error) {
|
||||||
// 1.使用模板文件作为输出文件
|
|
||||||
// 2.分销商总计使用第二行样式(宽高、背景、颜色等)
|
|
||||||
// 3.商品详情使用第三行样式(宽高、背景、颜色等)
|
|
||||||
// 4.保存为新文件
|
|
||||||
func (b *BbxtTools) resellerDetailFillExcel(templatePath, outputPath string, dataSlice []*ResellerLoss) error {
|
|
||||||
// 1. 读取模板
|
|
||||||
f, err := excelize.OpenFile(templatePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
sheet := f.GetSheetName(0)
|
var style = &excelize.Style{}
|
||||||
|
// 设置字体颜色
|
||||||
// 获取模板样式1:第二行-分销商总计
|
if colorHex, exists := styleMap["color"]; exists {
|
||||||
resellerTplRow := 2
|
style.Font = &excelize.Font{
|
||||||
styleIDReseller, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", resellerTplRow))
|
Color: colorHex,
|
||||||
if err != nil {
|
|
||||||
log.Errorf("获取分销商总计样式失败: %v", err)
|
|
||||||
styleIDReseller = 0
|
|
||||||
}
|
|
||||||
rowHeightReseller, err := f.GetRowHeight(sheet, resellerTplRow)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("获取分销商总计行高失败: %v", err)
|
|
||||||
rowHeightReseller = 31 // 默认高度
|
|
||||||
}
|
|
||||||
// 获取模板样式2:第三行-产品亏损明细
|
|
||||||
productTplRow := 3
|
|
||||||
styleIDProduct, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", productTplRow))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("获取商品详情样式失败: %v", err)
|
|
||||||
styleIDProduct = 0
|
|
||||||
}
|
|
||||||
rowHeightProduct, err := f.GetRowHeight(sheet, productTplRow)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("获取商品详情行高失败: %v", err)
|
|
||||||
rowHeightProduct = 25 // 默认高度
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRow := 2
|
|
||||||
|
|
||||||
for _, reseller := range dataSlice {
|
|
||||||
// 3. 填充经销商数据 (ResellerName, Total)
|
|
||||||
// 设置行高
|
|
||||||
f.SetRowHeight(sheet, currentRow, rowHeightReseller)
|
|
||||||
|
|
||||||
// 设置单元格值
|
|
||||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName)
|
|
||||||
f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), reseller.Total)
|
|
||||||
|
|
||||||
// 应用样式
|
|
||||||
if styleIDReseller != 0 {
|
|
||||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDReseller)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRow++
|
|
||||||
|
|
||||||
// 4. 填充产品亏损明细
|
|
||||||
// 先对 ProductLoss 进行排序
|
|
||||||
var products []ProductLoss
|
|
||||||
for _, p := range reseller.ProductLoss {
|
|
||||||
products = append(products, p)
|
|
||||||
}
|
|
||||||
// 按 Loss 升序排序 (亏损越多越靠前,负数越小)
|
|
||||||
sort.Slice(products, func(i, j int) bool {
|
|
||||||
return products[i].Loss < products[j].Loss
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, p := range products {
|
|
||||||
// 设置行高
|
|
||||||
f.SetRowHeight(sheet, currentRow, rowHeightProduct)
|
|
||||||
|
|
||||||
// 设置单元格值
|
|
||||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("·%s", p.ProductName))
|
|
||||||
f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.Loss)
|
|
||||||
|
|
||||||
// 应用样式
|
|
||||||
if styleIDProduct != 0 {
|
|
||||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow), styleIDProduct)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRow++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 设置水平对齐
|
||||||
|
if horizontal, exists := styleMap["horizontal"]; exists {
|
||||||
|
if style.Alignment == nil {
|
||||||
|
style.Alignment = &excelize.Alignment{}
|
||||||
|
}
|
||||||
|
style.Alignment.Horizontal = horizontal
|
||||||
|
}
|
||||||
|
// 设置垂直对齐
|
||||||
|
if vertical, exists := styleMap["vertical"]; exists {
|
||||||
|
if style.Alignment == nil {
|
||||||
|
style.Alignment = &excelize.Alignment{}
|
||||||
|
}
|
||||||
|
style.Alignment.Vertical = vertical
|
||||||
|
}
|
||||||
|
|
||||||
// 6. 保存
|
// 设置边框(新增)
|
||||||
return f.SaveAs(outputPath)
|
if borderColor, exists := styleMap["borderColor"]; exists {
|
||||||
|
style.Border = []excelize.Border{
|
||||||
|
{Type: "left", Color: borderColor, Style: 1}, // 左边框
|
||||||
|
{Type: "right", Color: borderColor, Style: 1}, // 右边框
|
||||||
|
{Type: "top", Color: borderColor, Style: 1}, // 上边框
|
||||||
|
{Type: "bottom", Color: borderColor, Style: 1}, // 下边框
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f.NewStyle(style)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分销商负利润详情填充excel-V2
|
// 分销商负利润详情填充excel-V2
|
||||||
|
|
@ -327,118 +302,6 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d
|
||||||
// 取消合并合计行的A、B列
|
// 取消合并合计行的A、B列
|
||||||
// f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow))
|
// f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow))
|
||||||
|
|
||||||
excelBytes, err := f.WriteToBuffer()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("write to bytes failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
picBytes, err := b.excel2picPy(templatePath, excelBytes.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("excel2picPy failed: %v", err)
|
|
||||||
}
|
|
||||||
// b.savePic("temp.png", picBytes) // 本地生成图片,仅测试
|
|
||||||
// outputPath 提取文件名(不包含扩展名)
|
|
||||||
filename := filepath.Base(outputPath)
|
|
||||||
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
|
||||||
imgUrl := b.uploadToOSS(filename, picBytes)
|
|
||||||
log.Infof("imgUrl: %s", imgUrl)
|
|
||||||
|
|
||||||
// 6. 保存
|
// 6. 保存
|
||||||
return f.SaveAs(outputPath)
|
return f.SaveAs(outputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// excel2picPy 将excel转换为图片python
|
|
||||||
// python 接口如下:
|
|
||||||
// curl --location --request POST 'http://192.168.6.109:8010/api/v1/convert' \
|
|
||||||
// --header 'Content-Type: multipart/form-data; boundary=--------------------------952147881043913664015069' \
|
|
||||||
// --form 'file=@"C:\\Users\\Administrator\\Downloads\\销售同比分析2025-12-29 0-12点.xlsx"' \
|
|
||||||
// --form 'sheet_name="销售同比分析"'
|
|
||||||
func (b *BbxtTools) excel2picPy(templatePath string, excelBytes []byte) ([]byte, error) {
|
|
||||||
// 1. 获取 Sheet Name
|
|
||||||
// 尝试从 excelBytes 解析,如果失败则使用默认值 "Sheet1"
|
|
||||||
sheetName := "Sheet1"
|
|
||||||
f, err := excelize.OpenReader(bytes.NewReader(excelBytes))
|
|
||||||
if err == nil {
|
|
||||||
sheetName = f.GetSheetName(0)
|
|
||||||
if sheetName == "" {
|
|
||||||
sheetName = "Sheet1"
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 构造 Multipart 请求
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
|
|
||||||
// 添加文件字段
|
|
||||||
// 使用 templatePath 的文件名作为上传文件名,如果没有则用 default.xlsx
|
|
||||||
filename := "default.xlsx"
|
|
||||||
if templatePath != "" {
|
|
||||||
filename = filepath.Base(templatePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
part, err := writer.CreateFormFile("file", filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create form file failed: %v", err)
|
|
||||||
}
|
|
||||||
if _, err = part.Write(excelBytes); err != nil {
|
|
||||||
return nil, fmt.Errorf("write file part failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加 sheet_name 字段
|
|
||||||
if err = writer.WriteField("sheet_name", sheetName); err != nil {
|
|
||||||
return nil, fmt.Errorf("write field sheet_name failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = writer.Close(); err != nil {
|
|
||||||
return nil, fmt.Errorf("close writer failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 发送 HTTP POST 请求
|
|
||||||
url := "http://192.168.6.109:8010/api/v1/convert"
|
|
||||||
req, err := http.NewRequest("POST", url, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create request failed: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("send request failed: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
|
||||||
return nil, fmt.Errorf("api request failed with status: %d, body: %s", resp.StatusCode, string(respBody))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 读取响应 Body (图片内容)
|
|
||||||
picBytes, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read response body failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return picBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// savePic 保存图片到本地
|
|
||||||
func (b *BbxtTools) savePic(outputPath string, picBytes []byte) error {
|
|
||||||
dir := filepath.Dir(outputPath)
|
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("create directory failed: %v", err)
|
|
||||||
}
|
|
||||||
return os.WriteFile(outputPath, picBytes, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
// uploadToOSS 上传至 oss 返回图片url
|
|
||||||
func (b *BbxtTools) uploadToOSS(fileName string, fileBytes []byte) string {
|
|
||||||
objectKey := fmt.Sprintf("ai-scheduler/data-analytics/images/%s.png", fileName)
|
|
||||||
url, err := b.ossClient.UploadBytes(objectKey, fileBytes)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("oss upload failed: %v", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
package bbxt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/pkg/utils_oss"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2/log"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Uploader struct {
|
||||||
|
ossClient *utils_oss.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
const RequestUrl = "http://192.168.6.109:8010/api/v1/convert"
|
||||||
|
|
||||||
|
func NewUploader(oss *utils_oss.Client) *Uploader {
|
||||||
|
return &Uploader{
|
||||||
|
ossClient: oss,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Uploader) Run(report *ReportRes) (err error) {
|
||||||
|
if len(report.Path) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err := excelize.OpenFile(report.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
excelBytes, err := f.WriteToBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write to bytes failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
picBytes, err := u.excel2picPy(report.Path, excelBytes.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("excel2picPy failed: %v", err)
|
||||||
|
}
|
||||||
|
// b.savePic("temp.png", picBytes) // 本地生成图片,仅测试
|
||||||
|
// outputPath 提取文件名(不包含扩展名)
|
||||||
|
filename := filepath.Base(report.Path)
|
||||||
|
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||||
|
report.Url = u.uploadToOSS(filename, picBytes)
|
||||||
|
log.Infof("imgUrl: %s", report.Url)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// excel2picPy 将excel转换为图片python
|
||||||
|
// python 接口如下:
|
||||||
|
// curl --location --request POST 'http://192.168.6.109:8010/api/v1/convert' \
|
||||||
|
// --header 'Content-Type: multipart/form-data; boundary=--------------------------952147881043913664015069' \
|
||||||
|
// --form 'file=@"C:\\Users\\Administrator\\Downloads\\销售同比分析2025-12-29 0-12点.xlsx"' \
|
||||||
|
// --form 'sheet_name="销售同比分析"'
|
||||||
|
func (u *Uploader) excel2picPy(templatePath string, excelBytes []byte) ([]byte, error) {
|
||||||
|
// 1. 获取 Sheet Name
|
||||||
|
// 尝试从 excelBytes 解析,如果失败则使用默认值 "Sheet1"
|
||||||
|
sheetName := "Sheet1"
|
||||||
|
f, err := excelize.OpenReader(bytes.NewReader(excelBytes))
|
||||||
|
if err == nil {
|
||||||
|
sheetName = f.GetSheetName(0)
|
||||||
|
if sheetName == "" {
|
||||||
|
sheetName = "Sheet1"
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构造 Multipart 请求
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
// 添加文件字段
|
||||||
|
// 使用 templatePath 的文件名作为上传文件名,如果没有则用 default.xlsx
|
||||||
|
filename := "default.xlsx"
|
||||||
|
if templatePath != "" {
|
||||||
|
filename = filepath.Base(templatePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
part, err := writer.CreateFormFile("file", filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create form file failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err = part.Write(excelBytes); err != nil {
|
||||||
|
return nil, fmt.Errorf("write file part failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 sheet_name 字段
|
||||||
|
if err = writer.WriteField("sheet_name", sheetName); err != nil {
|
||||||
|
return nil, fmt.Errorf("write field sheet_name failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writer.Close(); err != nil {
|
||||||
|
return nil, fmt.Errorf("close writer failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 发送 HTTP POST 请求
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", RequestUrl, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request failed: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("api request failed with status: %d, body: %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 读取响应 Body (图片内容)
|
||||||
|
picBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return picBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// savePic 保存图片到本地
|
||||||
|
func (u *Uploader) savePic(outputPath string, picBytes []byte) error {
|
||||||
|
dir := filepath.Dir(outputPath)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("create directory failed: %v", err)
|
||||||
|
}
|
||||||
|
return os.WriteFile(outputPath, picBytes, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// uploadToOSS 上传至 oss 返回图片url
|
||||||
|
func (u *Uploader) uploadToOSS(fileName string, fileBytes []byte) string {
|
||||||
|
objectKey := fmt.Sprintf("ai-scheduler/data-analytics/images/%s.png", fileName)
|
||||||
|
url, err := u.ossClient.UploadBytes(objectKey, fileBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("oss upload failed: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue