From 89323d325073e9e27c9b9109cd53d2288c42003d Mon Sep 17 00:00:00 2001
From: renzhiyuan <465386466@qq.com>
Date: Fri, 9 Jan 2026 11:00:39 +0800
Subject: [PATCH 01/13] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E7=A9=BA?=
=?UTF-8?q?=E5=80=BC=E6=A3=80=E6=9F=A5=E5=92=8C=E4=BC=98=E5=8C=96=E8=AE=A2?=
=?UTF-8?q?=E5=8D=95=E6=9F=A5=E8=AF=A2=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
internal/pkg/rec_extra/ext.go | 3 +++
internal/tools/zltx/excute_test.go | 21 ++++++++++++++---
internal/tools/zltx/zltx_order_detail.go | 29 ++++++++++++++++++++----
3 files changed, 46 insertions(+), 7 deletions(-)
diff --git a/internal/pkg/rec_extra/ext.go b/internal/pkg/rec_extra/ext.go
index b8b3c86..ac842de 100644
--- a/internal/pkg/rec_extra/ext.go
+++ b/internal/pkg/rec_extra/ext.go
@@ -17,6 +17,9 @@ func SetTaskRecExt(requireData *entitys.RequireData, rec *entitys.Recognize) {
}
func GetTaskRecExt(rec *entitys.Recognize) (ext entitys.TaskExt, err error) {
+ if rec == nil {
+ return
+ }
err = json.Unmarshal(rec.Ext, &ext)
return ext, err
}
diff --git a/internal/tools/zltx/excute_test.go b/internal/tools/zltx/excute_test.go
index 3f8460d..0c5cd78 100644
--- a/internal/tools/zltx/excute_test.go
+++ b/internal/tools/zltx/excute_test.go
@@ -4,16 +4,31 @@ import (
config2 "ai_scheduler/internal/config"
"ai_scheduler/internal/entitys"
"context"
+ "strings"
"testing"
)
func Test_task(t *testing.T) {
- c := NewZltxOrderDetailTool(config2.ToolConfig{}, nil)
+ c := NewZltxOrderDetailTool(config2.ToolConfig{
+ BaseURL: "https://revcl.1688sup.com/api/admin/direct/ai/%s",
+ AddURL: "https://revcl.1688sup.com/api/admin/direct/log/%s/%s",
+ }, nil)
+ ch := make(chan entitys.Response, 10000)
err :=
c.Execute(context.Background(), &entitys.Recognize{
Match: &entitys.Match{
- Parameters: `{"order_number": 859393216068067329}`,
+ Parameters: `{"order_number": 864086822064234497}`,
},
+ Ext: []byte(`{"auth":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzY3OTM0MTg1LCJuYmYiOjE3Njc5MjMzODUsImp0aSI6IjE3OSIsIlBob25lIjoiMTUwMDA0NTAxNTUiLCJVc2VyTmFtZSI6IjE1MDAwNDUwMTU1IiwiUmVhbE5hbWUiOiLliJjlvanlhpsiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9BRE1JTixWQ0xfU1lTVEVNLENSTV9BRE1JTixMU1hERFNfQURNSU4sTFNYRERTX1NZU1RFTSxNQVJLRVRJTkdTQUFTX1NVUEVSQURNSU4sU1RBVElTVElDQUxTWVNURU1fQURNSU4sUEhZU0lDQUxHT09EU19BRE1JTixNQVJLRVRJTkdTWVNURU1fU1VQRVIsUkVQT1JUQ0VOVEVSX0FETUlOLE1PTklUT1JfTUFESU4sWkxUWF9BRE1JTixaTFRYX09QRVJBVEUiLCJEaW5nVXNlcklkIjoiMTcxMTkzNTg3NTAzMjk5MzUifQ.d9z0S1Ia-PFAxhGstT055Amt8PI09bUHxG0_lba4UwvSomiTNCD-5DFdMkbZHwiDTlhVdBjcd1mDYFRRZXWMPoSnanubMBnRnuvTi8csch5nz1L9oWNo-HFyBE3lMw9-UJ5j84gz228_kcBsvRATT1Ixs9bnuaN9CDNz20c524llDt10C3cc8wLGMin4jWEMF4RNrf2oBZOFAahRYSJNeBmutIwRSIP1pMIAaUy_IkMCyOwK8JzgMnHJGLwIH_nxR9XZXlAN0FmrmtWVkRA2YUKvoDX5a5BCYmDVNqUbi_ZNuRPJH87Ia7_-UoyJu8Yq79jX0Qgsm6qJ4rX2nauneg"}`),
+ UserContent: &entitys.RecognizeUserContent{Text: "订单查询:864086822064234497\n"},
})
- t.Log(err)
+ if err != nil {
+ t.Log(err)
+ }
+ var res strings.Builder
+ for v := range ch {
+ res.WriteString(v.Content)
+ }
+ t.Log(res)
+
}
diff --git a/internal/tools/zltx/zltx_order_detail.go b/internal/tools/zltx/zltx_order_detail.go
index 47574a3..8ad1001 100644
--- a/internal/tools/zltx/zltx_order_detail.go
+++ b/internal/tools/zltx/zltx_order_detail.go
@@ -84,8 +84,8 @@ type ZltxOrderDetailData struct {
// Execute 执行直连天下订单详情查询
func (w *ZltxOrderDetailTool) Execute(ctx context.Context, rec *entitys.Recognize) error {
- var req ZltxOrderDetailRequest
- if err := json.Unmarshal([]byte(rec.Match.Parameters), &req); err != nil {
+ var req = &ZltxOrderDetailRequest{}
+ if err := req.UnmarshalJSON([]byte(rec.Match.Parameters)); err != nil {
return fmt.Errorf("invalid zltxOrderDetail request: %w", err)
}
if req.OrderNumber == "" {
@@ -96,6 +96,22 @@ func (w *ZltxOrderDetailTool) Execute(ctx context.Context, rec *entitys.Recogniz
return w.getZltxOrderDetail(rec, req.OrderNumber)
}
+func (r *ZltxOrderDetailRequest) UnmarshalJSON(data []byte) error {
+ var tmp struct {
+ OrderNumber json.Number `json:"order_number"` // 使用 json.Number 保留原始格式
+ }
+ if err := json.Unmarshal(data, &tmp); err != nil {
+ return err
+ }
+ // 根据需要转换为 int64 或 string
+ if num, err := tmp.OrderNumber.Int64(); err == nil {
+ r.OrderNumber = num
+ } else {
+ r.OrderNumber = tmp.OrderNumber.String()
+ }
+ return nil
+}
+
// getMockZltxOrderDetail 获取模拟直连天下订单详情数据
func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number interface{}) (err error) {
log.Infof("订单编号:%v,类型:%v")
@@ -170,8 +186,13 @@ func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number
err = w.llm.ChatStream(context.TODO(), rec.Ch, []api.Message{
{
- Role: "system",
- Content: "你是一个订单日志助手。用户可能会提供订单日志,你需要分析订单日志,失败订单->分析失败原因,成功订单->找出整个日志的 Base64 编码的 JSON 数据的内容进行转换并反馈给我",
+ Role: "system",
+ Content: "你是一个订单日志助手。用户可能会提供订单日志,你需要按以下规则处理:\n" +
+ "1. **先输出结论**:用标签包裹关键结论(如失败原因或Base64解码内容);\n" +
+ "2. **再输出分析过程**:详细解释如何得出结论;\n" +
+ "3. **订单类型处理**:\n" +
+ " - 失败订单:分析失败原因(如支付超时、库存不足等);\n" +
+ " - 成功订单:提取日志中的Base64编码JSON数据,解码后转换为用户可读的格式(如表格或JSON)。",
},
{
Role: "assistant",
From b4dddb06357e342836ffba58a564ce246d06f07e Mon Sep 17 00:00:00 2001
From: renzhiyuan <465386466@qq.com>
Date: Fri, 9 Jan 2026 16:52:05 +0800
Subject: [PATCH 02/13] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E7=BB=8F?=
=?UTF-8?q?=E9=94=80=E5=95=86=E9=BB=91=E5=90=8D=E5=8D=95=E6=95=B0=E6=8D=AE?=
=?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
internal/tools/bbxt/bbxt_test.go | 10 +++++-----
internal/tools/zltx/zltx_order_detail.go | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go
index e119c02..ee7f5da 100644
--- a/internal/tools/bbxt/bbxt_test.go
+++ b/internal/tools/bbxt/bbxt_test.go
@@ -23,7 +23,7 @@ func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
if err != nil {
panic(err)
}
- o, err := NewBbxtTools()
+ o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
@@ -34,7 +34,7 @@ func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
}
func Test_StatisOursProductLossSum(t *testing.T) {
- o, err := NewBbxtTools()
+ o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
@@ -45,7 +45,7 @@ func Test_StatisOursProductLossSum(t *testing.T) {
}
func Test_GetProfitRankingSum(t *testing.T) {
- o, err := NewBbxtTools()
+ o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
@@ -56,7 +56,7 @@ func Test_GetProfitRankingSum(t *testing.T) {
}
func Test_GetStatisOfficialProductSumDecline(t *testing.T) {
- o, err := NewBbxtTools()
+ o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
@@ -69,7 +69,7 @@ func Test_GetStatisOfficialProductSumDecline(t *testing.T) {
}
func Test_GetStatisOfficialProductSum(t *testing.T) {
- o, err := NewBbxtTools()
+ o, err := NewBbxtTools(nil)
if err != nil {
panic(err)
}
diff --git a/internal/tools/zltx/zltx_order_detail.go b/internal/tools/zltx/zltx_order_detail.go
index 8ad1001..6e33e63 100644
--- a/internal/tools/zltx/zltx_order_detail.go
+++ b/internal/tools/zltx/zltx_order_detail.go
@@ -189,7 +189,7 @@ func (w *ZltxOrderDetailTool) getZltxOrderDetail(rec *entitys.Recognize, number
Role: "system",
Content: "你是一个订单日志助手。用户可能会提供订单日志,你需要按以下规则处理:\n" +
"1. **先输出结论**:用标签包裹关键结论(如失败原因或Base64解码内容);\n" +
- "2. **再输出分析过程**:详细解释如何得出结论;\n" +
+ "2. **再输出分析过程**:详细解释如何得出结论;分析过程直接输出分析内容,不需要标明是分析过程\n" +
"3. **订单类型处理**:\n" +
" - 失败订单:分析失败原因(如支付超时、库存不足等);\n" +
" - 成功订单:提取日志中的Base64编码JSON数据,解码后转换为用户可读的格式(如表格或JSON)。",
From 44155c058d97263c683642076411112684579c4e Mon Sep 17 00:00:00 2001
From: fuzhongyun <15339891972@163.com>
Date: Fri, 9 Jan 2026 18:01:46 +0800
Subject: [PATCH 03/13] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E4=B8=80?=
=?UTF-8?q?=E4=B8=AA=E5=85=BC=E5=AE=B9=E7=9A=84=20TaskIndex=20=E5=AD=97?=
=?UTF-8?q?=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
internal/entitys/chat_history.go | 30 +++++++++++++++++++++++++++---
1 file changed, 27 insertions(+), 3 deletions(-)
diff --git a/internal/entitys/chat_history.go b/internal/entitys/chat_history.go
index b50148e..50bbb0b 100644
--- a/internal/entitys/chat_history.go
+++ b/internal/entitys/chat_history.go
@@ -33,9 +33,10 @@ type ChatHisQueryResponse struct {
Files string `gorm:"column:files;not null" json:"files"`
Useful int32 `gorm:"column:useful;not null;comment:0不评价,1有用,其他为无用" json:"useful"` // 0不评价,1有用,其他为无用
CreateAt string `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
- TaskID int32 `gorm:"column:task_id;not null" json:"task_id"` // 任务ID
- TaskName string `gorm:"column:task_name;not null" json:"task_name"` // 任务名称
- Contents []string `gorm:"column:contents" json:"contents"` // 前端回传数据
+ TaskID int32 `gorm:"column:task_id;not null" json:"task_id"` // 任务ID
+ TaskName string `gorm:"column:task_name;not null" json:"task_name"` // 任务名称
+ Contents []string `gorm:"column:contents" json:"contents"` // 前端回传数据
+ TaskIndex string `gorm:"column:task_index;not null" json:"task_index"` // 任务索引
}
func (c *ChatHisQueryResponse) FromModel(chat model.AiChatHi, task model.AiTask) {
@@ -49,6 +50,7 @@ func (c *ChatHisQueryResponse) FromModel(chat model.AiChatHi, task model.AiTask)
c.TaskID = chat.TaskID
c.TaskName = task.Name
c.Contents = make([]string, 0)
+ c.TaskIndex = c.parseTaskIndex(task.Index, chat.Ans)
// 解析Content
if "" != chat.Content {
@@ -60,6 +62,28 @@ func (c *ChatHisQueryResponse) FromModel(chat model.AiChatHi, task model.AiTask)
}
}
+func (c *ChatHisQueryResponse) parseTaskIndex(taskIndex string, ans string) string {
+ if taskIndex != "" {
+ return taskIndex
+ }
+
+ if ans == "" {
+ return ""
+ }
+ var resp []Response
+ err := json.Unmarshal([]byte(ans), &resp)
+ if err != nil {
+ log.Println("解析Ans失败 error: ", err)
+ return ""
+ }
+
+ if 0 < len(resp) {
+ return resp[0].Index
+ }
+
+ return ""
+}
+
type UpdateContentRequest struct {
HisID int64 `json:"his_id" validate:"required"`
Content string `json:"content" validate:"required"`
From 03652cb588eed711d4290eb8b0b7be9c82665722 Mon Sep 17 00:00:00 2001
From: fuzhongyun <15339891972@163.com>
Date: Fri, 9 Jan 2026 18:31:51 +0800
Subject: [PATCH 04/13] =?UTF-8?q?fix:=20session=20title=20=E8=A1=A5?=
=?UTF-8?q?=E5=85=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
internal/biz/do/ctx.go | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/internal/biz/do/ctx.go b/internal/biz/do/ctx.go
index 6058b38..8d5cf74 100644
--- a/internal/biz/do/ctx.go
+++ b/internal/biz/do/ctx.go
@@ -32,16 +32,18 @@ type Do struct {
}
func NewDo(
+ sessionImpl *impl.SessionImpl,
sysImpl *impl.SysImpl,
taskImpl *impl.TaskImpl,
hisImpl *impl.ChatHisImpl,
conf *config.Config,
) *Do {
return &Do{
- conf: conf,
- sysImpl: sysImpl,
- hisImpl: hisImpl,
- taskImpl: taskImpl,
+ conf: conf,
+ sessionImpl: sessionImpl,
+ sysImpl: sysImpl,
+ hisImpl: hisImpl,
+ taskImpl: taskImpl,
}
}
@@ -275,6 +277,15 @@ func (d *Do) startMessageHandler(
}
d.hisImpl.AddWithData(AiRes)
hisLog.HisId = AiRes.HisID
+
+ // 查询当前session
+ cond := builder.NewCond().And(builder.Eq{"session_id": requireData.Session})
+ sessionMap, _ := d.sessionImpl.GetOneBySearch(&cond)
+ requireData.SessionInfo.Title = sessionMap["title"].(string)
+ // 当前 session title为空 ,更新为用户输入
+ if requireData.SessionInfo.Title == "" {
+ d.sessionImpl.UpdateByCond(&cond, &model.AiSession{Title: requireData.Req.Text})
+ }
}
_ = entitys.MsgSend(client, entitys.Response{
From 6c987b15db6c5e7f731a37eda4bb6cf60eece7c1 Mon Sep 17 00:00:00 2001
From: renzhiyuan <465386466@qq.com>
Date: Tue, 13 Jan 2026 17:13:44 +0800
Subject: [PATCH 05/13] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E8=B4=9F?=
=?UTF-8?q?=E5=88=A9=E6=B6=A6=E5=88=86=E6=9E=90=E6=8A=A5=E8=A1=A8=E5=A4=84?=
=?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
config/config.yaml | 1 +
config/config_test.yaml | 14 +-
internal/biz/ding_talk_bot.go | 98 ++++++++---
internal/biz/group_config.go | 87 ++++++++--
internal/config/config.go | 6 +
internal/data/impl/provider_set.go | 1 +
internal/data/impl/report_dayily_cache.go | 17 ++
.../data/model/ai_report_daily_cache.gen.go | 20 +++
internal/pkg/lsxd/login.go | 29 ++--
internal/pkg/util/map.go | 31 +++-
internal/tools/bbxt/api.go | 123 ++++++++++++-
internal/tools/bbxt/bbxt.go | 161 ++++++++++++-----
internal/tools/bbxt/bbxt_test.go | 85 ++++++++-
internal/tools/bbxt/entitys.go | 15 +-
internal/tools/bbxt/excel.go | 164 +++++++++++++++++-
tmpl/excel_temp/kshj_total_ana.xlsx | Bin 0 -> 10304 bytes
16 files changed, 730 insertions(+), 122 deletions(-)
create mode 100644 internal/data/impl/report_dayily_cache.go
create mode 100644 internal/data/model/ai_report_daily_cache.gen.go
create mode 100644 tmpl/excel_temp/kshj_total_ana.xlsx
diff --git a/config/config.yaml b/config/config.yaml
index 37b65aa..3b4fab8 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -27,6 +27,7 @@ lsxd:
login_url: "https://api.user.1688sup.com/v1/login/phone"
phone: "ORlviZN7N06W2+WKLe76xg=="
password: "V5Uh8C4bamEM6UQZh4TCeQ=="
+ code: "456789"
check_token_url: "https://api.user.1688sup.com/v1/user/welcome"
diff --git a/config/config_test.yaml b/config/config_test.yaml
index c993b35..4aab8b8 100644
--- a/config/config_test.yaml
+++ b/config/config_test.yaml
@@ -26,10 +26,14 @@ coze:
lsxd:
# 统一登录
- login_url: "https://api.user.1688sup.com/v1/login/phone"
- phone: "ORlviZN7N06W2+WKLe76xg=="
- password: "V5Uh8C4bamEM6UQZh4TCeQ=="
- check_token_url: "https://api.user.1688sup.com/v1/user/welcome"
+ login_url: "http://api.test.user.1688sup.com/v1/login/phone"
+ phone: "OFJ8UpqOlI7+w3Qklf36ZA=="
+ password: "tEbFegH/DRRh6LutFb7o3g=="
+ code: "123456"
+ check_token_url: "http://api.test.user.1688sup.com/v1/user/welcome"
+
+zltx:
+ req_url: "https://gateway.dev.cdlsxd.cn/zltx_api"
sys:
session_len: 6
@@ -41,7 +45,7 @@ redis:
host: 47.97.27.195:6379
type: node
pass: lansexiongdi@666
- key: report-api-test
+ key: ai_scheduler-test
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
minIdleConns: 2 #最小空闲连接数
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭
diff --git a/internal/biz/ding_talk_bot.go b/internal/biz/ding_talk_bot.go
index 1ad715c..fe8c6e0 100644
--- a/internal/biz/ding_talk_bot.go
+++ b/internal/biz/ding_talk_bot.go
@@ -29,21 +29,22 @@ import (
// AiRouterBiz 智能路由服务
type DingTalkBotBiz struct {
- do *do.Do
- handle *do.Handle
- botConfigImpl *impl.BotConfigImpl
- replier *chatbot.ChatbotReplier
- log log.Logger
- dingTalkUser *dingtalk.User
- botGroupImpl *impl.BotGroupImpl
- botGroupConfigImpl *impl.BotGroupConfigImpl
- botGroupQywxImpl *impl.BotGroupQywxImpl
- toolManager *tools.Manager
- chatHis *impl.BotChatHisImpl
- conf *config.Config
- cardSend *dingtalk.SendCardClient
- qywxGroupHandle *qywx.Group
- groupConfigBiz *GroupConfigBiz
+ do *do.Do
+ handle *do.Handle
+ botConfigImpl *impl.BotConfigImpl
+ replier *chatbot.ChatbotReplier
+ log log.Logger
+ dingTalkUser *dingtalk.User
+ botGroupImpl *impl.BotGroupImpl
+ botGroupConfigImpl *impl.BotGroupConfigImpl
+ botGroupQywxImpl *impl.BotGroupQywxImpl
+ toolManager *tools.Manager
+ chatHis *impl.BotChatHisImpl
+ conf *config.Config
+ cardSend *dingtalk.SendCardClient
+ qywxGroupHandle *qywx.Group
+ groupConfigBiz *GroupConfigBiz
+ reportDailyCacheImpl *impl.ReportDailyCacheImpl
}
// NewDingTalkBotBiz
@@ -54,23 +55,25 @@ func NewDingTalkBotBiz(
botGroupImpl *impl.BotGroupImpl,
dingTalkUser *dingtalk.User,
chatHis *impl.BotChatHisImpl,
+ reportDailyCacheImpl *impl.ReportDailyCacheImpl,
toolManager *tools.Manager,
conf *config.Config,
cardSend *dingtalk.SendCardClient,
groupConfigBiz *GroupConfigBiz,
) *DingTalkBotBiz {
return &DingTalkBotBiz{
- do: do,
- handle: handle,
- botConfigImpl: botConfigImpl,
- replier: chatbot.NewChatbotReplier(),
- dingTalkUser: dingTalkUser,
- groupConfigBiz: groupConfigBiz,
- botGroupImpl: botGroupImpl,
- toolManager: toolManager,
- chatHis: chatHis,
- conf: conf,
- cardSend: cardSend,
+ do: do,
+ handle: handle,
+ botConfigImpl: botConfigImpl,
+ replier: chatbot.NewChatbotReplier(),
+ dingTalkUser: dingTalkUser,
+ groupConfigBiz: groupConfigBiz,
+ botGroupImpl: botGroupImpl,
+ toolManager: toolManager,
+ chatHis: chatHis,
+ conf: conf,
+ cardSend: cardSend,
+ reportDailyCacheImpl: reportDailyCacheImpl,
}
}
@@ -197,6 +200,49 @@ func (d *DingTalkBotBiz) Macro(ctx context.Context, requireData *entitys.Require
return
}
+ if strings.Contains(content, "[负利润分析]获取") {
+ var (
+ data model.AiReportDailyCache
+ value map[int32]*bbxt.ResellerLossSumProductRelation
+ )
+ cond := builder.NewCond()
+ cond = cond.And(builder.Eq{"`index`": bbxt.IndexLossSumDetail})
+ cond = cond.And(builder.Eq{"`key`": time.Now().Format(time.DateOnly)})
+ err = d.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &data)
+ if err != nil {
+ entitys.ResText(requireData.Ch, "", "获取失败")
+ return
+ }
+ err = json.Unmarshal([]byte(data.Value), &value)
+ if err != nil {
+ entitys.ResText(requireData.Ch, "", "获取失败,格式解析错误")
+ return
+ }
+
+ return
+ }
+
+ if strings.Contains(content, "[负利润分析]更新") {
+ // 提取冒号后的内容
+ if len(groupConfig.ProductName) == 0 {
+ entitys.ResText(requireData.Ch, "", "暂未设置")
+ } else {
+ entitys.ResText(requireData.Ch, "", groupConfig.ProductName)
+ isFinish = true
+ }
+ return
+ }
+
+ if strings.Contains(content, "[负利润分析]同步") {
+ // 提取冒号后的内容
+ if len(groupConfig.ProductName) == 0 {
+ entitys.ResText(requireData.Ch, "", "暂未设置")
+ } else {
+ entitys.ResText(requireData.Ch, "", groupConfig.ProductName)
+ isFinish = true
+ }
+ return
+ }
return
}
diff --git a/internal/biz/group_config.go b/internal/biz/group_config.go
index b8ccb92..21605fc 100644
--- a/internal/biz/group_config.go
+++ b/internal/biz/group_config.go
@@ -9,11 +9,15 @@ import (
"ai_scheduler/internal/domain/workflow/recharge"
"ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys"
+ "ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/l_request"
+ "ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/bbxt"
+ "ai_scheduler/utils"
"context"
+ "database/sql"
"encoding/json"
"errors"
"fmt"
@@ -30,12 +34,14 @@ import (
// AiRouterBiz 智能路由服务
type GroupConfigBiz struct {
- botGroupConfigImpl *impl.BotGroupConfigImpl
- ossClient *utils_oss.Client
- workflowManager *runtime.Registry
- botTools []model.AiBotTool
- toolManager *tools.Manager
- conf *config.Config
+ botGroupConfigImpl *impl.BotGroupConfigImpl
+ reportDailyCacheImpl *impl.ReportDailyCacheImpl
+ ossClient *utils_oss.Client
+ workflowManager *runtime.Registry
+ botTools []model.AiBotTool
+ toolManager *tools.Manager
+ conf *config.Config
+ rdb *utils.Rdb
}
// NewDingTalkBotBiz
@@ -45,13 +51,17 @@ func NewGroupConfigBiz(
botGroupConfigImpl *impl.BotGroupConfigImpl,
workflowManager *runtime.Registry,
conf *config.Config,
+ reportDailyCacheImpl *impl.ReportDailyCacheImpl,
+ rdb *utils.Rdb,
) *GroupConfigBiz {
return &GroupConfigBiz{
- botTools: tools.BootTools,
- ossClient: ossClient,
- botGroupConfigImpl: botGroupConfigImpl,
- workflowManager: workflowManager,
- conf: conf,
+ botTools: tools.BootTools,
+ ossClient: ossClient,
+ botGroupConfigImpl: botGroupConfigImpl,
+ workflowManager: workflowManager,
+ conf: conf,
+ reportDailyCacheImpl: reportDailyCacheImpl,
+ rdb: rdb,
}
}
@@ -72,12 +82,12 @@ func (g *GroupConfigBiz) GetReportLists(ctx context.Context, groupConfig *model.
product = strings.Split(groupConfig.ProductName, ",")
}
- reportList, err := bbxt.NewBbxtTools(g.conf)
+ reportList, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
if err != nil {
return
}
- reports, err = reportList.DailyReport(time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient)
+ reports, err = reportList.DailyReport(ctx, time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient, g.GetReportCache)
if err != nil {
return
}
@@ -145,7 +155,8 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
}
}
}
- rep, err := bbxt.NewBbxtTools(g.conf)
+
+ rep, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
uploader := bbxt.NewUploader(g.ossClient, g.conf)
if err != nil {
return err
@@ -153,7 +164,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
var reports []*bbxt.ReportRes
switch rec.Match.Index {
case "report_loss_analysis":
- repo, _err := rep.StatisOursProductLossSum(t)
+ repo, _err := rep.StatisOursProductLossSum(ctx, t, g.GetReportCache)
if _err != nil {
return _err
}
@@ -174,7 +185,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
reports = append(reports, repo)
case "report_daily":
product := strings.Split(groupConfig.ProductName, ",")
- repo, _err := rep.DailyReport(t, bbxt.DownWardValue, product, bbxt.SumFilter, nil)
+ repo, _err := rep.DailyReport(ctx, t, bbxt.DownWardValue, product, bbxt.SumFilter, nil, g.GetReportCache)
if _err != nil {
return _err
}
@@ -404,3 +415,47 @@ func (g *GroupConfigBiz) otherTask(ctx context.Context, rec *entitys.Recognize)
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
return
}
+
+func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, totalDetail []*bbxt.ResellerLoss, bbxtObj *bbxt.BbxtTools) error {
+ var ResellerProductRelation map[int32]*bbxt.ResellerLossSumProductRelation
+
+ dayDate := day.Format(time.DateOnly)
+ cond := builder.NewCond()
+ cond = cond.And(builder.Eq{"index": bbxt.IndexLossSumDetail})
+ cond = cond.And(builder.Eq{"key": dayDate})
+ var cache model.AiReportDailyCache
+ err := g.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
+ if err != nil {
+ if errors.Is(sql.ErrNoRows, err) {
+ ResellerProductRelation, err = bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
+ if err != nil {
+ return err
+ }
+ cache = model.AiReportDailyCache{
+ Key: dayDate,
+ Index: bbxt.IndexLossSumDetail,
+ Value: pkg.JsonStringIgonErr(ResellerProductRelation),
+ }
+ _, err = g.reportDailyCacheImpl.Add(&cache)
+ }
+ } else {
+ err = json.Unmarshal([]byte(cache.Value), &ResellerProductRelation)
+ }
+ if err != nil {
+ return err
+ }
+ for _, v := range totalDetail {
+ if _, ex := ResellerProductRelation[v.ResellerId]; !ex {
+ continue
+ }
+ v.Manager = ResellerProductRelation[v.ResellerId].AfterSaleName
+ for _, vv := range v.ProductLoss {
+ if _, ex := ResellerProductRelation[v.ResellerId].Products[vv.ProductId]; !ex {
+ continue
+ }
+ vv.LossReason = ResellerProductRelation[v.ResellerId].Products[vv.ProductId].LossReason
+ }
+ }
+
+ return nil
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 6756040..b4f05d9 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -27,6 +27,11 @@ type Config struct {
LLM LLM `mapstructure:"llm"`
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
Qywx QywxConfig `mapstructure:"qywx"`
+ ZLTX ZLTX `mapstructure:"zltx"`
+}
+
+type ZLTX struct {
+ ReqUrl string `mapstructure:"req_url"`
}
type SysPrompt struct {
@@ -136,6 +141,7 @@ type LSXDConfig struct {
LoginURL string `mapstructure:"login_url"`
Phone string `mapstructure:"phone"`
Password string `mapstructure:"password"`
+ Code string `mapstructure:"code"`
CheckTokenURL string `mapstructure:"check_token_url"`
}
diff --git a/internal/data/impl/provider_set.go b/internal/data/impl/provider_set.go
index 916b017..c200234 100644
--- a/internal/data/impl/provider_set.go
+++ b/internal/data/impl/provider_set.go
@@ -17,4 +17,5 @@ var ProviderImpl = wire.NewSet(
NewBotGroupImpl,
NewBotGroupConfigImpl,
NewBotGroupQywxImpl,
+ NewReportDailyCacheImpl,
)
diff --git a/internal/data/impl/report_dayily_cache.go b/internal/data/impl/report_dayily_cache.go
new file mode 100644
index 0000000..479d888
--- /dev/null
+++ b/internal/data/impl/report_dayily_cache.go
@@ -0,0 +1,17 @@
+package impl
+
+import (
+ "ai_scheduler/internal/data/model"
+ "ai_scheduler/tmpl/dataTemp"
+ "ai_scheduler/utils"
+)
+
+type ReportDailyCacheImpl struct {
+ dataTemp.DataTemp
+}
+
+func NewReportDailyCacheImpl(db *utils.Db) *ReportDailyCacheImpl {
+ return &ReportDailyCacheImpl{
+ DataTemp: *dataTemp.NewDataTemp(db, new(model.AiReportDailyCache)),
+ }
+}
diff --git a/internal/data/model/ai_report_daily_cache.gen.go b/internal/data/model/ai_report_daily_cache.gen.go
new file mode 100644
index 0000000..0fa1f57
--- /dev/null
+++ b/internal/data/model/ai_report_daily_cache.gen.go
@@ -0,0 +1,20 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package model
+
+const TableNameAiReportDailyCache = "ai_report_daily_cache"
+
+// AiReportDailyCache mapped from table
+type AiReportDailyCache struct {
+ ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
+ Key string `gorm:"column:key;not null;default:1;comment:索引方式,可以是任意类型" json:"key"` // 索引方式,可以是任意类型
+ Value string `gorm:"column:value;comment:类型下所需路由以及参数" json:"value"` // 类型下所需路由以及参数
+ Index string `gorm:"column:index;not null;comment:类型" json:"index"` // 类型
+}
+
+// TableName AiReportDailyCache's table name
+func (*AiReportDailyCache) TableName() string {
+ return TableNameAiReportDailyCache
+}
diff --git a/internal/pkg/lsxd/login.go b/internal/pkg/lsxd/login.go
index 5ea6801..13b7f1c 100644
--- a/internal/pkg/lsxd/login.go
+++ b/internal/pkg/lsxd/login.go
@@ -95,30 +95,18 @@ func (l *Login) checkTokenValid(ctx context.Context, token string) bool {
// 调用登录接口获取token
func (l *Login) login(ctx context.Context) (string, error) {
- // 1.获取配置
- loginURL := l.config.LSXD.LoginURL
- phone := l.config.LSXD.Phone
- password := l.config.LSXD.Password
-
- // 2.调用登录接口获取token
- if loginURL == "" {
- return "", errors.New("login url is empty")
- }
- if phone == "" || password == "" {
- return "", errors.New("phone or password is empty")
- }
reqBody := map[string]any{
- "phone": phone,
- "password": password,
- "code": "456789",
+ "phone": l.config.LSXD.Phone,
+ "password": l.config.LSXD.Password,
+ "code": l.config.LSXD.Code,
}
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return "", err
}
- status, respBody, err := l.doRequestWithBody(ctx, http.MethodPost, loginURL, "", "application/json", bodyBytes)
+ status, respBody, err := l.doRequestWithBody(ctx, http.MethodPost, l.config.LSXD.LoginURL, "", "application/json", bodyBytes)
if err != nil {
return "", err
}
@@ -158,7 +146,7 @@ func (l *Login) login(ctx context.Context) (string, error) {
}
func (l *Login) getCachedToken(ctx context.Context) (string, error) {
- token, err := l.redisCli.Get(ctx, constants.CACHE_KEY_LSXD_TOKEN).Result()
+ token, err := l.redisCli.Get(ctx, l.getCacheKey()).Result()
if err == nil {
return token, nil
}
@@ -168,11 +156,16 @@ func (l *Login) getCachedToken(ctx context.Context) (string, error) {
return "", err
}
+func (l *Login) getCacheKey() string {
+ return l.config.Redis.Key + constants.CACHE_KEY_LSXD_TOKEN + l.config.LSXD.Phone // 1.获取配置
+
+}
+
func (l *Login) cacheToken(ctx context.Context, token string) error {
if token == "" {
return errors.New("token is empty")
}
- return l.redisCli.Set(ctx, constants.CACHE_KEY_LSXD_TOKEN, token, constants.EXPIRE_LSXD_TOKEN).Err()
+ return l.redisCli.Set(ctx, l.getCacheKey(), token, constants.EXPIRE_LSXD_TOKEN).Err()
}
func (l *Login) doRequest(ctx context.Context, method string, url string, authorization string, body []byte) (int, error) {
diff --git a/internal/pkg/util/map.go b/internal/pkg/util/map.go
index 5ca80c8..8bc5e1d 100644
--- a/internal/pkg/util/map.go
+++ b/internal/pkg/util/map.go
@@ -1,6 +1,10 @@
package util
-import "encoding/json"
+import (
+ "encoding/json"
+ "reflect"
+ "strings"
+)
// StructToMap 将结构体转换为 map[string]any
func StructToMap(v any) (map[string]any, error) {
@@ -12,3 +16,28 @@ func StructToMap(v any) (map[string]any, error) {
err = json.Unmarshal(b, &m)
return m, err
}
+
+func StructToMapWithReflect(obj interface{}) map[string]interface{} {
+ val := reflect.ValueOf(obj)
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+ if val.Kind() != reflect.Struct {
+ return nil
+ }
+
+ data := make(map[string]interface{})
+ for i := 0; i < val.NumField(); i++ {
+ valueField := val.Field(i)
+ typeField := val.Type().Field(i)
+ jsonTag := typeField.Tag.Get("json")
+ if idx := strings.Index(jsonTag, ","); idx != -1 {
+ jsonTag = jsonTag[:idx]
+ }
+ if !typeField.IsExported() {
+ continue
+ }
+ data[jsonTag] = valueField.Interface()
+ }
+ return data
+}
diff --git a/internal/tools/bbxt/api.go b/internal/tools/bbxt/api.go
index a7b057b..4dd645b 100644
--- a/internal/tools/bbxt/api.go
+++ b/internal/tools/bbxt/api.go
@@ -3,6 +3,8 @@ package bbxt
import (
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/l_request"
+ "ai_scheduler/internal/pkg/util"
+
"encoding/json"
"fmt"
"net/http"
@@ -29,12 +31,23 @@ type StatisOursProductLossSumResponse struct {
}
const Base = "https://reportapi.1688sup.com/api"
+const AuthUrl = "http://test.analysis.com/api"
// StatisOursProductLossSumApi 负利润分析
func StatisOursProductLossSumApi(param *StatisOursProductLossSumReq) (*StatisOursProductLossSumRes, error) {
url := "/dataanalytics/statisOursProductLossSum"
var res StatisOursProductLossSumRes
- if err := request(url, param, &res); err != nil {
+ if err := request(url, param, &res, ""); err != nil {
+ return nil, err
+ }
+ return &res, nil
+}
+
+// StatisOursProductLossSumApi 负利润分析
+func StatisOursProductLossSumApiWithAuth(param *StatisOursProductLossSumReq, token string) (*StatisOursProductLossSumRes, error) {
+ url := "/dataanalytics/statisOursProductLossSum"
+ var res StatisOursProductLossSumRes
+ if err := request(url, param, &res, token); err != nil {
return nil, err
}
return &res, nil
@@ -73,7 +86,7 @@ type ProfitRankingSumResponse struct {
func GetProfitRankingSumApi(param *GetProfitRankingSumRequest) (*GetProfitRankingSumResponse, error) {
url := "/dataanalytics/profitRankingSum"
var res GetProfitRankingSumResponse
- if err := request(url, param, &res); err != nil {
+ if err := request(url, param, &res, ""); err != nil {
return nil, err
}
return &res, nil
@@ -106,7 +119,7 @@ type GetStatisOfficialProductSum struct {
func GetStatisOfficialProductSumApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumResponse, error) {
url := "/dataanalytics/statisOfficialProduct"
var res GetStatisOfficialProductSumResponse
- if err := request(url, param, &res); err != nil {
+ if err := request(url, param, &res, ""); err != nil {
return nil, err
}
return &res, nil
@@ -133,7 +146,7 @@ type GetStatisOfficialProductSumDecline struct {
func GetStatisOfficialProductSumDeclineApi(param *GetStatisOfficialProductSumRequest) (*GetStatisOfficialProductSumDeclineResponse, error) {
url := "/dataanalytics/statisOfficialProductDecline"
var res GetStatisOfficialProductSumDeclineResponse
- if err := request(url, param, &res); err != nil {
+ if err := request(url, param, &res, ""); err != nil {
return nil, err
}
return &res, nil
@@ -162,23 +175,119 @@ type StatisFilterOfficialProductResponse struct {
func GetStatisFilterOfficialProductApi(param *GetStatisFilterOfficialProductRequest) (*GetStatisFilterOfficialProductResponse, error) {
url := "/dataanalytics/statisFilterOfficialProduct"
var res GetStatisFilterOfficialProductResponse
- if err := request(url, param, &res); err != nil {
+ if err := request(url, param, &res, ""); err != nil {
return nil, err
}
return &res, nil
}
-func request(url string, reqData interface{}, resData interface{}) error {
+//type GetManagerAndDefaultLossReasonRequest struct {
+// ResellerId int32 ` json:"reseller_id"`
+// GoodsIds int32 ` json:"reseller_id"`
+//}
+type GetManagerAndDefaultLossReasonRequest struct {
+ Param map[int32]map[string][]int32 ` json:"param"`
+}
+
+type GetManagerAndDefaultLossReasonResponse struct {
+ Res []*GetManagerAndDefaultLossReasonResponseList `json:"res,omitempty"`
+}
+
+type GetManagerAndDefaultLossReasonResponseList struct {
+ ResellerInfo *GetManagerAndDefaultLossReasonResponse_ResellerInfo `json:"GetManagerAndDefaultLossReasonResponse_ResellerInfo,omitempty"`
+ ProductList []*GetManagerAndDefaultLossReasonResponse_ProductList `json:"GetManagerAndDefaultLossReasonResponse_ProductList,omitempty"`
+}
+
+type GetManagerAndDefaultLossReasonResponse_ResellerInfo struct {
+ ResellerId int32 `json:"reseller_id,omitempty"`
+ AfterSaleName string `json:"after_sale_name,omitempty"`
+}
+
+type GetManagerAndDefaultLossReasonResponse_ProductList struct {
+ ProductId int32 `json:"product_id,omitempty"`
+ LossReason string `json:"loss_reason,omitempty"`
+}
+
+// GetStatisFilterOfficialProductApi 官方商品列表
+func GetManagerAndDefaultLossReasonApi(param *GetManagerAndDefaultLossReasonRequest, token string, reqUrl string) ([]*GetManagerAndDefaultLossReasonResponseList, error) {
+ return []*GetManagerAndDefaultLossReasonResponseList{
+ {
+ ResellerInfo: &GetManagerAndDefaultLossReasonResponse_ResellerInfo{
+ ResellerId: 25009,
+ AfterSaleName: "张三",
+ },
+ ProductList: []*GetManagerAndDefaultLossReasonResponse_ProductList{
+ {
+ ProductId: 129,
+ LossReason: "小米钱包h5-QQ音乐绿钻季卡原因",
+ },
+ {
+ ProductId: 2218,
+ LossReason: "小米钱包h5-百度网盘新vip会员月卡原因",
+ },
+ {
+ ProductId: 3226,
+ LossReason: "小米钱包h5-腾讯视频月卡-小米钱包2024原因",
+ },
+ {
+ ProductId: 3364,
+ LossReason: "小米钱包h5-腾讯视频月卡-0元直充原因",
+ },
+ },
+ },
+ }, nil
+
+ reqParam, err := util.StructToMap(param)
+ if err != nil {
+ return nil, err
+ }
+
+ req := &l_request.Request{
+ Url: reqUrl + "/admin/reseller/resellerAuthProduct/getManagerAndDefaultLossReason",
+ Method: http.MethodPost,
+ Json: reqParam,
+ Headers: map[string]string{
+ "Authorization": fmt.Sprintf("Bearer %s", token),
+ },
+ }
+ res, err := req.Send()
+ if err != nil {
+ return nil, err
+ }
+ if res.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("request failed, status code: %d,resion: %s", res.StatusCode, res.Reason)
+ }
+ var code resCode
+ if err = json.Unmarshal(res.Content, &code); err != nil {
+ return nil, fmt.Errorf("返回结构异常:%s", string(res.Content))
+ }
+ var resData []*GetManagerAndDefaultLossReasonResponseList
+ if err = json.Unmarshal(code.Data, &resData); err != nil {
+ return nil, fmt.Errorf("返回数据异常:%s", string(res.Content))
+ }
+ return resData, nil
+}
+
+func request(url string, reqData interface{}, resData interface{}, token string) error {
+ requestSchema := Base
+ if len(token) > 0 {
+ requestSchema = AuthUrl
+ }
reqParam, err := pkg.StructToURLValues(reqData)
if err != nil {
return err
}
req := &l_request.Request{
- Url: FormatPHPURL(Base+url, reqParam),
+ Url: FormatPHPURL(requestSchema+url, reqParam),
Method: http.MethodGet,
}
+ if len(token) > 0 {
+ req.Headers = map[string]string{
+ "Authorization": fmt.Sprintf("Bearer %s", token),
+ }
+ }
res, err := req.Send()
if err != nil {
return err
diff --git a/internal/tools/bbxt/bbxt.go b/internal/tools/bbxt/bbxt.go
index 304be24..926f973 100644
--- a/internal/tools/bbxt/bbxt.go
+++ b/internal/tools/bbxt/bbxt.go
@@ -3,8 +3,10 @@ package bbxt
import (
"ai_scheduler/internal/config"
pkginner "ai_scheduler/internal/pkg"
+ "ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/pkg"
+ "context"
"fmt"
"math/rand"
"slices"
@@ -18,18 +20,17 @@ const (
GreenStyle = "${color: 008000;horizontal:center;vertical:center;borderColor:#000000}"
)
+const (
+ IndexLossSumDetail = "lossSumDetail"
+)
+
+type LossSumInitFunc func(ctx context.Context, day time.Time, totalDetail []*ResellerLoss, selfObj *BbxtTools) error
+
var (
DownWardValue int32 = 1000
SumFilter int32 = -150
)
-var resellerBlackList = []string{
- "悦跑",
- "电商-独立",
- "蓝星严选连续包月",
- "通钱-2025年12月",
-}
-
var resellerBlackListProduct = []string{
"悦跑",
"电商-独立",
@@ -43,9 +44,10 @@ type BbxtTools struct {
excelTempDir string
ossClient *utils_oss.Client
config *config.Config
+ login *lsxd.Login
}
-func NewBbxtTools(config *config.Config) (*BbxtTools, error) {
+func NewBbxtTools(config *config.Config, login *lsxd.Login) (*BbxtTools, error) {
cache, err := pkg.GetCacheDir()
if err != nil {
return nil, err
@@ -59,12 +61,21 @@ func NewBbxtTools(config *config.Config) (*BbxtTools, error) {
cacheDir: cache,
excelTempDir: fmt.Sprintf("%s/excel_temp", tempDir),
config: config,
+ login: login,
}, nil
}
-func (b *BbxtTools) DailyReport(now time.Time, downWardValue int32, productName []string, sumFilter int32, ossClient *utils_oss.Client) (reports []*ReportRes, err error) {
+func (b *BbxtTools) DailyReport(
+ ctx context.Context,
+ now time.Time,
+ downWardValue int32,
+ productName []string,
+ sumFilter int32,
+ ossClient *utils_oss.Client,
+ initFunc LossSumInitFunc,
+) (reports []*ReportRes, err error) {
reports = make([]*ReportRes, 0, 4)
- productLossReport, err := b.StatisOursProductLossSum(now)
+ productLossReport, err := b.StatisOursProductLossSum(ctx, now, initFunc)
if err != nil {
return
}
@@ -94,7 +105,7 @@ func (b *BbxtTools) DailyReport(now time.Time, downWardValue int32, productName
}
// StatisOursProductLossSum 负利润分析
-func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes, err error) {
+func (b *BbxtTools) StatisOursProductLossSum(ctx context.Context, now time.Time, initFunc LossSumInitFunc) (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(now), //adjustedTime(time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())),
@@ -110,6 +121,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
resellerMap = make(map[int32]*ResellerLoss)
total [][]string
gt []*ResellerLoss
+ totalDetail []*ResellerLoss
)
for _, info := range data.List {
@@ -119,8 +131,8 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
resellerMap[info.ResellerId] = &ResellerLoss{
ResellerId: info.ResellerId,
ResellerName: info.ResellerName,
- Total: 0, // 初始化为0,后续累加
- ProductLoss: make(map[int32]ProductLoss), // 初始化map
+ Total: 0, // 初始化为0,后续累加
+ ProductLoss: make(map[int32]*ProductLoss), // 初始化map
}
}
@@ -133,7 +145,7 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
// 检查产品是否已存在
if _, ok := reseller.ProductLoss[info.OursProductId]; !ok {
// 创建新的产品亏损记录
- reseller.ProductLoss[info.OursProductId] = ProductLoss{
+ reseller.ProductLoss[info.OursProductId] = &ProductLoss{
ProductId: info.OursProductId,
ProductName: info.OursProductName,
Loss: info.Loss, // 初始化为当前产品的亏损
@@ -155,7 +167,8 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
return resellers[i].Total < resellers[j].Total
})
var (
- totalSum float64
+ totalSum float64
+
totalSum500 float64
)
// 构建分组
@@ -166,46 +179,116 @@ func (b *BbxtTools) StatisOursProductLossSum(now time.Time) (report []*ReportRes
fmt.Sprintf("%.2f", v.Total),
})
totalSum += v.Total
+ totalDetail = append(totalDetail, v)
}
if v.Total <= -500 && !slices.Contains(resellerBlackListProduct, v.ResellerName) {
gt = append(gt, v)
totalSum500 += v.Total
}
}
- report = make([]*ReportRes, 2)
+ report = make([]*ReportRes, 3)
timeCh := now.Format("1月2日15点")
//总量生成excel
- if len(total) > 0 {
- filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
- err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total, "")
- report[0] = &ReportRes{
- ReportName: "负利润分析(合计表)",
- Title: "截至" + timeCh + "利润累计亏损" + fmt.Sprintf("%.2f", totalSum),
- Path: filePath,
- Data: total,
- }
- }
+ //if len(total) > 0 {
+ // filePath := b.cacheDir + "/kshj_total" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
+ // err = b.SimpleFillExcelWithTitle(b.excelTempDir+"/"+"kshj_total.xlsx", filePath, total, "")
+ // if err != nil {
+ // return
+ // }
+ // report[0] = &ReportRes{
+ // ReportName: "分销商负利润统计",
+ // Title: "截至" + timeCh + "利润累计亏损" + fmt.Sprintf("%.2f", totalSum),
+ // Path: filePath,
+ // Data: total,
+ // }
+ //}
- if err != nil {
- return
- }
- if len(gt) > 0 {
- filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
- title := "截至" + timeCh + "亏损500以上的分销商和产品"
- err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt, title)
- report[1] = &ReportRes{
- ReportName: "负利润分析(亏损500以上)",
- Title: "截至" + timeCh + "亏损500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500),
+ //if len(gt) > 0 {
+ // filePath := b.cacheDir + "/kshj_gt" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
+ // title := "截至" + timeCh + "亏损500以上的分销商和产品"
+ // err = b.resellerDetailFillExcelV2(b.excelTempDir+"/"+"kshj_gt.xlsx", filePath, gt, title)
+ // if err != nil {
+ // return
+ // }
+ // report[1] = &ReportRes{
+ // ReportName: "负利润分析(亏损500以上)",
+ // Title: "截至" + timeCh + "亏损500以上利润累计亏损" + fmt.Sprintf("%.2f", totalSum500),
+ // Path: filePath,
+ // Data: total,
+ // }
+ //}
+
+ if len(totalDetail) > 0 {
+ err = initFunc(ctx, now, totalDetail, b)
+ if err != nil {
+ return
+ }
+ filePath := b.cacheDir + "/kshj_total_ana" + fmt.Sprintf("%d%d", time.Now().Unix(), rand.Intn(1000)) + ".xlsx"
+ title := "截至" + timeCh + "亏损100以上的分销商&产品负利润原因"
+ err = b.resellerDetailFillExcelAna(b.excelTempDir+"/"+"kshj_total_ana.xlsx", filePath, totalDetail, title)
+ if err != nil {
+ return
+ }
+ report[2] = &ReportRes{
+ ReportName: "负利润分析(亏损100以上)",
+ Title: "截至" + timeCh + "亏损100以上利润原因",
Path: filePath,
Data: total,
}
}
- if err != nil {
- return
- }
return report, nil
}
+func (b *BbxtTools) GetResellerLossMannagerAndLossReasonFromApi(ctx context.Context, resellerLoss []*ResellerLoss) (map[int32]*ResellerLossSumProductRelation, error) {
+ var (
+ resellerMap = make(map[int32]map[string][]int32)
+ resellerLossMap = make(map[int32]*ResellerLoss, len(resellerLoss))
+ relationMap = make(map[int32]*ResellerLossSumProductRelation)
+ )
+
+ for _, v := range resellerLoss {
+ var productSlice = make([]int32, 0, len(v.ProductLoss))
+ for _, vv := range v.ProductLoss {
+ productSlice = append(productSlice, vv.ProductId)
+ }
+ if _, ex := resellerMap[v.ResellerId]; !ex {
+ resellerMap[v.ResellerId] = make(map[string][]int32, 1)
+ }
+ resellerMap[v.ResellerId]["values"] = productSlice
+ resellerLossMap[v.ResellerId] = v
+ relationMap[v.ResellerId] = &ResellerLossSumProductRelation{
+ ResellerName: v.ResellerName,
+ Products: make(map[int32]*LossReason, len(v.ProductLoss)),
+ }
+ for _, product := range v.ProductLoss {
+ relationMap[v.ResellerId].Products[product.ProductId] = &LossReason{
+ ProductName: product.ProductName,
+ LossReason: "未填写", // 初始化为未填写
+ }
+ }
+ }
+ res, err := GetManagerAndDefaultLossReasonApi(&GetManagerAndDefaultLossReasonRequest{
+ Param: resellerMap,
+ }, b.login.GetToken(ctx), b.config.ZLTX.ReqUrl)
+ if err != nil {
+ return nil, err
+ }
+ for _, v := range res {
+ if v == nil {
+ continue
+ }
+ if _, ok := resellerLossMap[v.ResellerInfo.ResellerId]; !ok {
+ continue
+ }
+ relationMap[v.ResellerInfo.ResellerId].AfterSaleName = v.ResellerInfo.AfterSaleName
+
+ for _, vv := range v.ProductList {
+ relationMap[v.ResellerInfo.ResellerId].Products[vv.ProductId].LossReason = vv.LossReason
+ }
+ }
+ return relationMap, nil
+}
+
// GetProfitRankingSum 利润同比分销商排行榜
func (b *BbxtTools) GetProfitRankingSum(now time.Time) (report *ReportRes, err error) {
diff --git a/internal/tools/bbxt/bbxt_test.go b/internal/tools/bbxt/bbxt_test.go
index ee7f5da..1ea8f43 100644
--- a/internal/tools/bbxt/bbxt_test.go
+++ b/internal/tools/bbxt/bbxt_test.go
@@ -2,10 +2,19 @@ package bbxt
import (
"ai_scheduler/internal/config"
+ "ai_scheduler/internal/data/impl"
+ "ai_scheduler/internal/data/model"
+ "ai_scheduler/internal/pkg"
+ "ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss"
+ "ai_scheduler/utils"
+ "context"
+ "encoding/json"
"strings"
"testing"
"time"
+
+ "xorm.io/builder"
)
func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
@@ -23,29 +32,30 @@ func Test_StatisOursProductLossSumApiTotal(t *testing.T) {
if err != nil {
panic(err)
}
- o, err := NewBbxtTools(nil)
+ o, err := NewBbxtTools(nil, nil)
if err != nil {
panic(err)
}
- reports, err := o.DailyReport(time.Now(), DownWardValue, []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, SumFilter, ossClient)
+ reports, err := o.DailyReport(context.Background(), time.Now(), DownWardValue, []string{"官方-爱奇艺-星钻季卡", "官方-爱奇艺-星钻半年卡", "官方--腾讯-年卡", "官方--爱奇艺-月卡"}, SumFilter, ossClient, GetReportCache)
t.Log(reports, err)
}
func Test_StatisOursProductLossSum(t *testing.T) {
- o, err := NewBbxtTools(nil)
+ run()
+ o, err := NewBbxtTools(configConfig, lsxd.NewLogin(configConfig, utils.NewRdb(configConfig)))
if err != nil {
panic(err)
}
- report, err := o.StatisOursProductLossSum(time.Now())
+ report, err := o.StatisOursProductLossSum(context.Background(), time.Now(), GetReportCache)
t.Log(report, err)
}
func Test_GetProfitRankingSum(t *testing.T) {
- o, err := NewBbxtTools(nil)
+ o, err := NewBbxtTools(nil, nil)
if err != nil {
panic(err)
}
@@ -56,7 +66,7 @@ func Test_GetProfitRankingSum(t *testing.T) {
}
func Test_GetStatisOfficialProductSumDecline(t *testing.T) {
- o, err := NewBbxtTools(nil)
+ o, err := NewBbxtTools(nil, nil)
if err != nil {
panic(err)
}
@@ -69,7 +79,9 @@ func Test_GetStatisOfficialProductSumDecline(t *testing.T) {
}
func Test_GetStatisOfficialProductSum(t *testing.T) {
- o, err := NewBbxtTools(nil)
+
+ configs := configConfig
+ o, err := NewBbxtTools(nil, lsxd.NewLogin(configs, utils.NewRdb(configConfig)))
if err != nil {
panic(err)
}
@@ -79,3 +91,62 @@ func Test_GetStatisOfficialProductSum(t *testing.T) {
t.Log(report, err)
}
+
+var (
+ reportDailyCacheImpl *impl.ReportDailyCacheImpl
+ configConfig *config.Config
+)
+
+func run() {
+ configConfig, _ = config.LoadConfigWithTest()
+ // 初始化数据库连接
+ db, _ := utils.NewGormDb(configConfig)
+ reportDailyCacheImpl = impl.NewReportDailyCacheImpl(db)
+}
+
+func GetReportCache(ctx context.Context, day time.Time, totalDetail []*ResellerLoss, bbxtObj *BbxtTools) error {
+ run()
+
+ var ResellerProductRelation map[int32]*ResellerLossSumProductRelation
+ dayDate := day.Format(time.DateOnly)
+ cond := builder.NewCond()
+ cond = cond.And(builder.Eq{"`index`": IndexLossSumDetail})
+ cond = cond.And(builder.Eq{"`key`": dayDate})
+ var cache model.AiReportDailyCache
+
+ err := reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
+ if err != nil {
+ return err
+ }
+ if cache.Value == "" {
+ ResellerProductRelation, err = bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
+ if err != nil {
+ return err
+ }
+ cache = model.AiReportDailyCache{
+ Key: dayDate,
+ Index: IndexLossSumDetail,
+ Value: pkg.JsonStringIgonErr(ResellerProductRelation),
+ }
+ _, err = reportDailyCacheImpl.Add(&cache)
+ if err != nil {
+ return err
+ }
+ }
+ err = json.Unmarshal([]byte(cache.Value), &ResellerProductRelation)
+
+ for _, v := range totalDetail {
+ if _, ex := ResellerProductRelation[v.ResellerId]; !ex {
+ continue
+ }
+ v.Manager = ResellerProductRelation[v.ResellerId].AfterSaleName
+ for _, vv := range v.ProductLoss {
+ if _, ex := ResellerProductRelation[v.ResellerId].Products[vv.ProductId]; !ex {
+ continue
+ }
+ vv.LossReason = ResellerProductRelation[v.ResellerId].Products[vv.ProductId].LossReason
+ }
+ }
+
+ return nil
+}
diff --git a/internal/tools/bbxt/entitys.go b/internal/tools/bbxt/entitys.go
index 7729d68..904f541 100644
--- a/internal/tools/bbxt/entitys.go
+++ b/internal/tools/bbxt/entitys.go
@@ -4,13 +4,15 @@ type ResellerLoss struct {
ResellerId int32
ResellerName string
Total float64
- ProductLoss map[int32]ProductLoss
+ ProductLoss map[int32]*ProductLoss
+ Manager string
}
type ProductLoss struct {
ProductId int32
ProductName string
Loss float64
+ LossReason string
}
type ReportRes struct {
@@ -37,3 +39,14 @@ type ProductSumReseller struct {
HistoryTwoNum int32 //上周成功数量
HistoryTwoDiff int32 //同比上周当前增减量
}
+
+type ResellerLossSumProductRelation struct {
+ AfterSaleName string `json:"after_sale_name"`
+ ResellerName string `json:"reseller_name"`
+ Products map[int32]*LossReason
+}
+
+type LossReason struct {
+ ProductName string
+ LossReason string
+}
diff --git a/internal/tools/bbxt/excel.go b/internal/tools/bbxt/excel.go
index 1744f76..91915dc 100644
--- a/internal/tools/bbxt/excel.go
+++ b/internal/tools/bbxt/excel.go
@@ -242,7 +242,7 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d
for _, reseller := range dataSlice {
// 排序 ProductLoss
- var products []ProductLoss
+ var products []*ProductLoss
for _, p := range reseller.ProductLoss {
products = append(products, p)
}
@@ -311,4 +311,164 @@ func (b *BbxtTools) resellerDetailFillExcelV2(templatePath, outputPath string, d
return f.SaveAs(outputPath)
}
-// OfficialProductSumDeclineExcel
+func (b *BbxtTools) resellerDetailFillExcelAna(templatePath, outputPath string, dataSlice []*ResellerLoss, title string) error {
+ // 1. 读取模板
+ f, err := excelize.OpenFile(templatePath)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ sheet := f.GetSheetName(0)
+ if len(title) > 0 {
+ // 写入标题
+ f.SetCellValue(sheet, "A1", title)
+ }
+ // ---------------- 样式获取 ----------------
+ // 模板第2行:数据行样式
+ tplRowData := 3
+ styleA3, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowData))
+ if err != nil {
+ styleA3 = 0
+ }
+ // B2和C2通常样式一致,这里取B2作为明细列样式
+ styleB3, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowData))
+ if err != nil {
+ styleB3 = 0
+ }
+ styleC3, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowData))
+ if err != nil {
+ styleC3 = 0
+ }
+
+ styleD3, err := f.GetCellStyle(sheet, fmt.Sprintf("D%d", tplRowData))
+ if err != nil {
+ styleC3 = 0
+ }
+
+ styleE3, err := f.GetCellStyle(sheet, fmt.Sprintf("E%d", tplRowData))
+ if err != nil {
+ styleC3 = 0
+ }
+
+ rowHeightData, err := f.GetRowHeight(sheet, tplRowData)
+ if err != nil {
+ rowHeightData = 20
+ }
+
+ // 模板第4行:合计行样式
+ tplRowTotal := 5
+ styleTotalA, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", tplRowTotal))
+ if err != nil {
+ styleTotalA = 0
+ }
+ styleTotalB, err := f.GetCellStyle(sheet, fmt.Sprintf("B%d", tplRowTotal))
+ if err != nil {
+ styleTotalB = 0
+ }
+ styleTotalC, err := f.GetCellStyle(sheet, fmt.Sprintf("C%d", tplRowTotal))
+ if err != nil {
+ styleTotalC = 0
+ }
+ styleTotalD, err := f.GetCellStyle(sheet, fmt.Sprintf("D%d", tplRowTotal))
+ if err != nil {
+ styleTotalC = 0
+ }
+ styleTotalE, err := f.GetCellStyle(sheet, fmt.Sprintf("E%d", tplRowTotal))
+ if err != nil {
+ styleTotalC = 0
+ }
+ rowHeightTotal, err := f.GetRowHeight(sheet, tplRowTotal)
+ if err != nil {
+ rowHeightTotal = 30
+ }
+ // ----------------------------------------
+
+ currentRow := 3
+ totalLoss := 0.0
+
+ for _, reseller := range dataSlice {
+ // 排序 ProductLoss
+ var products []*ProductLoss
+ for _, p := range reseller.ProductLoss {
+ products = append(products, p)
+ }
+ sort.Slice(products, func(i, j int) bool {
+ return products[i].Loss < products[j].Loss
+ })
+
+ startRow := currentRow
+
+ // 填充该经销商的所有产品
+ for _, p := range products {
+ // 设置行高
+ f.SetRowHeight(sheet, currentRow, rowHeightData)
+
+ // 设置值
+ f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), reseller.ResellerName)
+ f.SetCellValue(sheet, fmt.Sprintf("B%d", currentRow), p.ProductName)
+ f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), p.Loss)
+ f.SetCellValue(sheet, fmt.Sprintf("D%d", currentRow), reseller.Manager)
+ f.SetCellValue(sheet, fmt.Sprintf("E%d", currentRow), p.LossReason)
+ // 设置样式
+ if styleA3 != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleA3)
+ }
+ if styleB3 != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleB3)
+ }
+ if styleC3 != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleC3)
+ }
+ if styleD3 != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("D%d", currentRow), fmt.Sprintf("D%d", currentRow), styleD3)
+ }
+ if styleE3 != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("E%d", currentRow), fmt.Sprintf("E%d", currentRow), styleE3)
+ }
+
+ totalLoss += p.Loss
+ currentRow++
+ }
+
+ endRow := currentRow - 1
+ // 合并单元格 (如果多于1行)
+ if endRow > startRow {
+ f.MergeCell(sheet, fmt.Sprintf("A%d", startRow), fmt.Sprintf("A%d", endRow))
+ f.MergeCell(sheet, fmt.Sprintf("D%d", startRow), fmt.Sprintf("D%d", endRow))
+ }
+ }
+
+ // ---------------- 填充合计行 ----------------
+ // 四舍五入保留四位小数
+ totalLoss, _ = decimal.NewFromFloat(totalLoss).Round(4).Float64()
+ // 设置行高
+ f.SetRowHeight(sheet, currentRow, rowHeightTotal)
+
+ f.SetCellValue(sheet, fmt.Sprintf("A%d", currentRow), "合计")
+ // B列留空,C列填充总亏损
+ f.SetCellValue(sheet, fmt.Sprintf("C%d", currentRow), totalLoss)
+
+ // 设置合计行样式
+ if styleTotalA != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("A%d", currentRow), styleTotalA)
+ }
+ if styleTotalB != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("B%d", currentRow), fmt.Sprintf("B%d", currentRow), styleTotalB)
+ }
+ if styleTotalC != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("C%d", currentRow), fmt.Sprintf("C%d", currentRow), styleTotalC)
+ }
+ if styleTotalD != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("D%d", currentRow), fmt.Sprintf("D%d", currentRow), styleTotalD)
+ }
+ if styleTotalE != 0 {
+ f.SetCellStyle(sheet, fmt.Sprintf("E%d", currentRow), fmt.Sprintf("E%d", currentRow), styleTotalE)
+ }
+
+ // 取消合并合计行的A、B列
+ // f.MergeCell(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("B%d", currentRow))
+
+ // 6. 保存
+ return f.SaveAs(outputPath)
+}
diff --git a/tmpl/excel_temp/kshj_total_ana.xlsx b/tmpl/excel_temp/kshj_total_ana.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..d2774f36304f5afde22664eb808ace7d75651753
GIT binary patch
literal 10304
zcma)iWk4KT5-#pCxVyW%yIXJx?(P!YCAbFnP7I1KVyG`Cp#BnT;h3*v
z1O@_P1pxvA{9Vkz-k#pw#yT_Zi!>+`s@P4yGoZ$fg^u_QGgUws52Th09*f;Vrz~>(
z&iFkYy))4*r0u1h$`3del=0$5M>oCW@9h^**>-7Zb!&kQlZ5mn8
z(Uj1Xa#U7UgoWBr&h*ze?(5~CG1wsCXyjYYQM;y*;PoNmAr5tN&eR7u;SQHHrlry}
zoYngUz=76jCZTZbPz6DbTkWtGmKpW8tCN!@(F{8DpXotheGYZXlX~3q8>!K^F4p%p
zpMq=Gc?3Y`m)}Yr1+LMJYAKrYLEp3M^5<`9LF{5XEl5}9f0*DE?HTy*=n9zWoGZMbYx{yO(%;cFvU4AUYBaL#Y#z!AQ3=Vq`Sly)T
zQweP?OPdfSTE4<#5O~3Il5JsIo$5G_IY36H$Av{pQi&U1Sd`)uf``DcVxBO{Q9hl$
zt6F&E8mB=iRC^i^lJ9F&Za@v>Y`&3?>TR$|U_USl`hnU}j-UNdQq8ybQH7UeQJ&G8RvTCY!nD|pf$%ZmJT;||I5joHV}>=gWN-iBZZiWA}C
zG-o7!JA5)|6E%^Z)Rdj#B(aKT<~9pzbwe86ZmH`I=7+e;cE>}(o+=|L2r}6VatBHD
zKE5D$;{*CT`cZnHgT-FZ4|+i#69R8bfp#E2Rn?!N(NXqYq+m@kg<1;^+`#kH9;+H1)5#pZh{;iZ47?qEmRp1E$Pz)@0rouhME9(E8ctBrJ
zcenm4<@~b%!cPG=J4Y)ga}yKiKhqO~;AkuFi+?8iPDt0D%_srKy-8eoP!U^=ILRiC8Rd_zN%G1nxLqJ&T`LxT?Y?A#{jnagaBI8ZiQdY3BSOA&$K~hC0
z$<1tOq%gYb9-ce`Q!`?6i++T@h=`BUoA;wFBxg@N15(sd5N!dtGoYTmS*WKhsa|3g
zz!AuW@w)xVTON_|Kx-Q@{9MJd?5`8Dh9jN^
zSEOJ3NQ8y$+6&m*2GUf+A${aq076(G3{=x*9A_5~^6qhs47(3Aj!eBoYGE?8MAGHp
zHwU2U-S#el;%)?5F5a3ZtE-zC0csHpS0t>pgG!c0hTCAKz{2OV+@Ne9z<2`vVgk{s
ztHN~c6+>^pAnoxonm?D=u=kEfkzv*?Gz9PJcS)_un{``a^>?{$j$(pHHo+4%Hb^mm
z^j~@s12r~dj}NPF3whdL%kH>p$e{F;AH)PyrLuFiROA<872Ll6*5I(a#KaOZLQ4rH
zuNejTfmNlfdK~PA;xarS`0-
zrD3Pq*s@6x(=k%7w#x(PCn3Nttlr22s8P*ZKf>CZoT#eHL%qZ+f&csRecd;_EMI4H
z6C0C1_6*GY4jXC*$(cap_R2BoNT2JBb^YDG
zu-l?)Lc{)qR7NDun7c7g%LshE1SzSgkWOr)gP>r(+Qyrwv|wV&H_zZ%M*}_bxVh*-
zIhOJ)S*xBt1RXa&;%L+fNLe|*>Hw12yC%O}G9aa|fh#<<8FmjKZ9N|t4g170o<5%G
zh&tnnEIP?3sRUwql6Bej7(9vq{