From 85b552eec34a920061e005f9a34ea8e7e46fd65c Mon Sep 17 00:00:00 2001 From: danial Date: Thu, 11 Dec 2025 20:13:52 +0800 Subject: [PATCH] feat(camel_oil): add order export to Excel functionality - Add ExportOrder RPC method to camel_oil API and service interfaces - Implement service logic to query orders and generate Excel file with order data - Include card number and password fields in order export - Create HTTP handler to stream Excel file with proper headers for download - Handle token status update on frequent error ban (oneDay case) - Fix order processing query to filter by status and pay status correctly - Add new error code for one-day ban in camel_oil_api and handle in client logic - Update order model and response to include card number and password - Remove redundant logging of SendCaptcha request data in camel_oil_api client - Add access control checks on ExportOrder endpoint for authorized users only --- api/camel_oil/camel_oil.go | 2 + api/camel_oil/v1/order.go | 17 ++ .../camel_oil/camel_oil_v1_create_token.go | 2 +- .../camel_oil/camel_oil_v1_export_order.go | 40 +++++ internal/logic/camel_oil/cron_tasks.go | 16 +- internal/logic/camel_oil/order.go | 2 +- internal/logic/camel_oil/order_query.go | 165 ++++++++++++++++++ internal/logic/camel_oil/token.go | 7 + internal/service/camel_oil.go | 2 + utility/integration/camel_oil_api/api.go | 8 +- utility/integration/camel_oil_api/api_test.go | 2 +- 11 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 internal/controller/camel_oil/camel_oil_v1_export_order.go diff --git a/api/camel_oil/camel_oil.go b/api/camel_oil/camel_oil.go index ec3280a7..8940c0a2 100644 --- a/api/camel_oil/camel_oil.go +++ b/api/camel_oil/camel_oil.go @@ -15,6 +15,7 @@ type ICamelOilV1 interface { CheckAccount(ctx context.Context, req *v1.CheckAccountReq) (res *v1.CheckAccountRes, err error) AccountHistory(ctx context.Context, req *v1.AccountHistoryReq) (res *v1.AccountHistoryRes, err error) AccountStatistics(ctx context.Context, req *v1.AccountStatisticsReq) (res *v1.AccountStatisticsRes, err error) + DeleteExpiredAccounts(ctx context.Context, req *v1.DeleteExpiredAccountsReq) (res *v1.DeleteExpiredAccountsRes, err error) SubmitOrder(ctx context.Context, req *v1.SubmitOrderReq) (res *v1.SubmitOrderRes, err error) ListOrder(ctx context.Context, req *v1.ListOrderReq) (res *v1.ListOrderRes, err error) OrderDetail(ctx context.Context, req *v1.OrderDetailReq) (res *v1.OrderDetailRes, err error) @@ -22,6 +23,7 @@ type ICamelOilV1 interface { AccountOrderList(ctx context.Context, req *v1.AccountOrderListReq) (res *v1.AccountOrderListRes, err error) OrderCallback(ctx context.Context, req *v1.OrderCallbackReq) (res *v1.OrderCallbackRes, err error) ListPrefetchOrder(ctx context.Context, req *v1.ListPrefetchOrderReq) (res *v1.ListPrefetchOrderRes, err error) + ExportOrder(ctx context.Context, req *v1.ExportOrderReq) (res *v1.ExportOrderRes, err error) GetPrefetchOrderLogs(ctx context.Context, req *v1.GetPrefetchOrderLogsReq) (res *v1.GetPrefetchOrderLogsRes, err error) GetSettings(ctx context.Context, req *v1.GetSettingsReq) (res *v1.GetSettingsRes, err error) UpdateSettings(ctx context.Context, req *v1.UpdateSettingsReq) (res *v1.UpdateSettingsRes, err error) diff --git a/api/camel_oil/v1/order.go b/api/camel_oil/v1/order.go index 788566f3..e9375c0d 100644 --- a/api/camel_oil/v1/order.go +++ b/api/camel_oil/v1/order.go @@ -42,6 +42,8 @@ type OrderListItem struct { AccountName string `json:"accountName" description:"账号名称"` Amount float64 `json:"amount" description:"订单金额"` AlipayUrl string `json:"alipayUrl" description:"支付宝支付链接"` + CardNumber string `json:"cardNumber" description:"提取的卡号"` + CardPassword string `json:"cardPassword" description:"提取的卡密"` Status consts.CamelOilOrderStatus `json:"status" description:"订单状态"` PayStatus consts.CamelOilPayStatus `json:"payStatus" description:"支付状态"` NotifyStatus consts.CamelOilNotifyStatus `json:"notifyStatus" description:"回调状态"` @@ -185,3 +187,18 @@ type PrefetchOrderListItem struct { type ListPrefetchOrderRes struct { commonApi.CommonPageRes[PrefetchOrderListItem] } + +// ExportOrderReq 导出订单数据 +type ExportOrderReq struct { + g.Meta `path:"/jd-v2/order/export" tags:"JD V2 Order" method:"get" summary:"导出订单数据"` + commonApi.CommonPageReq + MerchantOrderId string `json:"merchantOrderId" description:"商户订单号"` + OrderNo string `json:"orderNo" description:"系统订单号"` + AccountId int64 `json:"accountId" description:"账号ID"` + Status consts.CamelOilOrderStatus `json:"status" description:"订单状态"` + PayStatus consts.CamelOilPayStatus `json:"payStatus" description:"支付状态"` + DateRange []*gtime.Time `json:"dateRange" description:"时间范围"` +} + +type ExportOrderRes struct { +} diff --git a/internal/controller/camel_oil/camel_oil_v1_create_token.go b/internal/controller/camel_oil/camel_oil_v1_create_token.go index eccc5c12..d00cc672 100644 --- a/internal/controller/camel_oil/camel_oil_v1_create_token.go +++ b/internal/controller/camel_oil/camel_oil_v1_create_token.go @@ -13,7 +13,7 @@ import ( func (c *ControllerV1) CreateToken(ctx context.Context, req *v1.CreateTokenReq) (res *v1.CreateTokenRes, err error) { _, err = service.SysAuth().LoginOnlyLogin(ctx) if err != nil { - err = errHandler.WrapError(ctx, gcode.CodeInternalError, err, "登录校验失败") + err = errHandler.WrapError(ctx, gcode.CodeInternalError, err, "权限不足,只允许核销上传") return } createInput := &model.CamelOilTokenCreateInput{ diff --git a/internal/controller/camel_oil/camel_oil_v1_export_order.go b/internal/controller/camel_oil/camel_oil_v1_export_order.go new file mode 100644 index 00000000..555bfd6e --- /dev/null +++ b/internal/controller/camel_oil/camel_oil_v1_export_order.go @@ -0,0 +1,40 @@ +package camel_oil + +import ( + "context" + "fmt" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/frame/g" + + v1 "kami/api/camel_oil/v1" + "kami/internal/errHandler" + "kami/internal/service" +) + +func (c *ControllerV1) ExportOrder(ctx context.Context, req *v1.ExportOrderReq) (res *v1.ExportOrderRes, err error) { + // 权限检查 + _, err = service.SysAuth().LoginOnlyIFrame(ctx) + if err != nil { + err = errHandler.WrapError(ctx, gcode.CodeNotAuthorized, err, "权限不足") + return + } + + // 调用service层方法获取Excel文件内容 + fileName, content, err := service.CamelOil().ExportOrder(ctx, req) + if err != nil { + return nil, err + } + + // 设置响应头,告知浏览器这是一个文件下载 + r := g.RequestFromCtx(ctx) + r.Response.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + r.Response.Header().Set("Content-Disposition", "attachment; filename="+fileName) + r.Response.Header().Set("Content-Length", fmt.Sprintf("%d", len(content))) + + // 直接写入响应体 + r.Response.Write(content) + + // 返回一个空的响应,因为数据已经直接写入到响应体中 + return &v1.ExportOrderRes{}, nil +} \ No newline at end of file diff --git a/internal/logic/camel_oil/cron_tasks.go b/internal/logic/camel_oil/cron_tasks.go index f92a42b3..7019b3f2 100644 --- a/internal/logic/camel_oil/cron_tasks.go +++ b/internal/logic/camel_oil/cron_tasks.go @@ -87,6 +87,7 @@ func (s *sCamelOil) CronOrderPaymentCheckTask(ctx context.Context) error { // 查询待支付订单(创建时间在24小时内) var orders []*entity.V1CamelOilOrder err := dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()). + Where(dao.V1CamelOilOrder.Columns().Status, consts.CamelOilOrderStatusProcessing). Where(dao.V1CamelOilOrder.Columns().PayStatus, consts.CamelOilPaymentStatusUnpaid). WhereGTE(dao.V1CamelOilOrder.Columns().CreatedAt, gtime.Now().Add(-gtime.D)). Scan(&orders) @@ -123,14 +124,12 @@ func (s *sCamelOil) CronOrderPaymentCheckTask(ctx context.Context) error { } // 订单已支付 - if queryResult != nil { - if order.PayStatus != int(consts.CamelOilPaymentStatusPaid) { - glog.Infof(ctx, "订单%s已支付,金额: %.2f", order.OrderNo, queryResult.Balance) - _ = s.fillOrderCard(ctx, order.OrderNo, queryResult.CardNumber, queryResult.CardPassword) - // 增加账户订单计数 - _ = s.IncrementAccountOrderCount(ctx, order.AccountId) - paidCount++ - } + if queryResult != nil && order.PayStatus != int(consts.CamelOilPaymentStatusPaid) { + glog.Infof(ctx, "订单%s已支付,金额: %.2f", order.OrderNo, queryResult.Balance) + _ = s.fillOrderCard(ctx, order.OrderNo, queryResult.CardNumber, queryResult.CardPassword) + // 增加账户订单计数 + _ = s.IncrementAccountOrderCount(ctx, order.AccountId) + paidCount++ continue } @@ -306,6 +305,7 @@ func (s *sCamelOil) CronCardBindingTask(ctx context.Context) error { var orders []*entity.V1CamelOilOrder err := dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()). Where(dao.V1CamelOilOrder.Columns().PayStatus, consts.CamelOilPaymentStatusPaid). + Where(dao.V1CamelOilOrder.Columns().Status, consts.CamelOilOrderStatusProcessing). WhereNotIn(dao.V1CamelOilOrder.Columns().Id, dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1()). Fields(dao.V1CamelOilCardBinding.Columns().OrderId)). diff --git a/internal/logic/camel_oil/order.go b/internal/logic/camel_oil/order.go index 4c0c0839..1db9f899 100644 --- a/internal/logic/camel_oil/order.go +++ b/internal/logic/camel_oil/order.go @@ -134,7 +134,7 @@ func (s *sCamelOil) SubmitOrder(ctx context.Context, req *v1.SubmitOrderReq) (re // ==================================================================================== // FillOrderCard 填写订单卡密和卡号 -func (s *sCamelOil) fillOrderCard(ctx context.Context, orderNo string, cardPassword string, cardNumber string) error { +func (s *sCamelOil) fillOrderCard(ctx context.Context, orderNo string, cardNumber string, cardPassword string) error { // 1. 查询订单信息 var order *entity.V1CamelOilOrder err := dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()). diff --git a/internal/logic/camel_oil/order_query.go b/internal/logic/camel_oil/order_query.go index 5db76bd4..1916bd5c 100644 --- a/internal/logic/camel_oil/order_query.go +++ b/internal/logic/camel_oil/order_query.go @@ -2,9 +2,13 @@ package camel_oil import ( "context" + "fmt" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/glog" + "github.com/xuri/excelize/v2" + "github.com/gogf/gf/v2/os/gtime" v1 "kami/api/camel_oil/v1" "kami/internal/consts" "kami/internal/dao" @@ -65,6 +69,8 @@ func (s *sCamelOil) ListOrder(ctx context.Context, req *v1.ListOrderReq) (res *v AccountName: order.AccountName, Amount: order.Amount.InexactFloat64(), AlipayUrl: order.AlipayUrl, + CardNumber: order.CardNumber, + CardPassword: order.CardPassword, Status: consts.CamelOilOrderStatus(order.Status), PayStatus: consts.CamelOilPayStatus(order.PayStatus), NotifyStatus: consts.CamelOilNotifyStatus(order.NotifyStatus), @@ -215,6 +221,165 @@ func (s *sCamelOil) ListPrefetchOrder(ctx context.Context, req *v1.ListPrefetchO // 辅助函数 // ==================================================================================== +// ExportOrder 导出订单数据为Excel +func (s *sCamelOil) ExportOrder(ctx context.Context, req *v1.ExportOrderReq) (fileName string, content []byte, err error) { + // 构建查询条件 + m := dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()) + + if req.MerchantOrderId != "" { + m = m.Where(dao.V1CamelOilOrder.Columns().MerchantOrderId, req.MerchantOrderId) + } + if req.OrderNo != "" { + m = m.Where(dao.V1CamelOilOrder.Columns().OrderNo, req.OrderNo) + } + if req.AccountId != 0 { + m = m.Where(dao.V1CamelOilOrder.Columns().AccountId, req.AccountId) + } + if req.Status > 0 { + m = m.Where(dao.V1CamelOilOrder.Columns().Status, int(req.Status)) + } + if req.PayStatus > 0 { + m = m.Where(dao.V1CamelOilOrder.Columns().PayStatus, int(req.PayStatus)) + } + if len(req.DateRange) == 2 && req.DateRange[0] != nil && req.DateRange[1] != nil { + m = m.WhereBetween(dao.V1CamelOilOrder.Columns().CreatedAt, req.DateRange[0], req.DateRange[1]) + } + + // 查询所有符合条件的订单 + var orders []*entity.V1CamelOilOrder + err = m.OrderDesc(dao.V1CamelOilOrder.Columns().CreatedAt).Scan(&orders) + if err != nil { + return "", nil, gerror.Wrap(err, "查询订单列表失败") + } + + // 收集所有账号ID + accountIds := make([]int64, 0, len(orders)) + accountMap := make(map[int64]*entity.V1CamelOilAccount) + for _, order := range orders { + if order.AccountId > 0 { + accountIds = append(accountIds, order.AccountId) + } + } + + // 批量查询账号信息 + if len(accountIds) > 0 { + var accounts []*entity.V1CamelOilAccount + err = dao.V1CamelOilAccount.Ctx(ctx).DB(config.GetDatabaseV1()). + WhereIn(dao.V1CamelOilAccount.Columns().Id, accountIds). + Scan(&accounts) + if err == nil { + for _, account := range accounts { + accountMap[account.Id] = account + } + } + } + + // 创建Excel文件 + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + glog.Error(ctx, "关闭Excel文件失败", err) + } + }() + + // 设置工作表名称 + sheetName := "骆驼油订单数据" + f.SetSheetName("Sheet1", sheetName) + + // 设置表头 + headers := []string{"订单ID", "系统订单号", "商户订单号", "手机号", "卡号", "卡密", "订单金额", "订单状态", "支付状态", "创建时间"} + for i, header := range headers { + cell := fmt.Sprintf("%s1", string(rune('A'+i))) + f.SetCellValue(sheetName, cell, header) + } + + // 填充数据 + for i, order := range orders { + row := i + 2 + + // 订单ID + f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), order.Id) + + // 系统订单号 + f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), order.OrderNo) + + // 商户订单号 + f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), order.MerchantOrderId) + + // 手机号 + phone := "" + if account, exists := accountMap[order.AccountId]; exists && account != nil { + phone = account.Phone + } + f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), phone) + + // 卡号 + f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), order.CardNumber) + + // 卡密 + f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), order.CardPassword) + + // 订单金额 + f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), order.Amount.InexactFloat64()) + + // 订单状态 + statusText := s.getOrderStatusText(consts.CamelOilOrderStatus(order.Status)) + f.SetCellValue(sheetName, fmt.Sprintf("H%d", row), statusText) + + // 支付状态 + payStatusText := s.getPayStatusText(consts.CamelOilPayStatus(order.PayStatus)) + f.SetCellValue(sheetName, fmt.Sprintf("I%d", row), payStatusText) + + // 创建时间 + if order.CreatedAt != nil { + f.SetCellValue(sheetName, fmt.Sprintf("J%d", row), order.CreatedAt.Format("Y-m-d H:i:s")) + } + } + + // 生成文件名 + fileName = fmt.Sprintf("骆驼油订单数据_%s.xlsx", gtime.Now().Format("Y-m-d_H-i-s")) + + // 保存到字节数组 + buf, err := f.WriteToBuffer() + if err != nil { + return "", nil, gerror.Wrap(err, "生成Excel文件失败") + } + + return fileName, buf.Bytes(), nil +} + +// getOrderStatusText 获取订单状态文本 +func (s *sCamelOil) getOrderStatusText(status consts.CamelOilOrderStatus) string { + switch status { + case consts.CamelOilOrderStatusPending: + return "待处理" + case consts.CamelOilOrderStatusProcessing: + return "处理中" + case consts.CamelOilOrderStatusCompleted: + return "已完成" + case consts.CamelOilOrderStatusFailed: + return "已失败" + default: + return "未知" + } +} + +// getPayStatusText 获取支付状态文本 +func (s *sCamelOil) getPayStatusText(status consts.CamelOilPayStatus) string { + switch status { + case consts.CamelOilPaymentStatusUnpaid: + return "未支付" + case consts.CamelOilPaymentStatusPaid: + return "已支付" + case consts.CamelOilPaymentStatusRefunded: + return "已退款" + case consts.CamelOilPaymentStatusTimeout: + return "已超时" + default: + return "未知" + } +} + // maskPhone 手机号脱敏 func maskPhone(phone string) string { if len(phone) < 11 { diff --git a/internal/logic/camel_oil/token.go b/internal/logic/camel_oil/token.go index 3b0c19d2..f47ec935 100644 --- a/internal/logic/camel_oil/token.go +++ b/internal/logic/camel_oil/token.go @@ -254,6 +254,7 @@ func (s *sCamelOil) UpdateTokenStatus(ctx context.Context, req *model.CamelOilTo // 更新 Token 状态 _, err = m.Where(dao.V1CamelOilToken.Columns().Id, req.TokenId).Update(&do.V1CamelOilToken{ Status: int(req.NewStatus), + Remark: req.Remark, }) if err != nil { @@ -318,6 +319,12 @@ func (s *sCamelOil) BindCardToToken(ctx context.Context, req *model.CamelOilCard // 调用已实现的方法更新订单状态 _ = s.UpdateOrderStatus(ctx, orderId, consts.CamelOilOrderStatusFailed, consts.CamelOilOrderChangeTypeFail, "", "卡密样检失败") return 0, gerror.Wrap(rechargeErr, "卡密样检失败") + case camel_oil_api.RechargeCardBannedOneDay: + _ = s.UpdateTokenStatus(ctx, &model.CamelOilTokenStatusUpdateInput{ + TokenId: selectedToken.Id, + NewStatus: consts.CamelOilTokenStatusDisabled, + Remark: "输错过于频繁,Token被封禁", + }) case camel_oil_api.RechargeCardErrorToken: // Token 过期/无效:标记 Token 为已过期 glog.Warningf(ctx, "Token 过期: %v", rechargeErr) diff --git a/internal/service/camel_oil.go b/internal/service/camel_oil.go index 558aa9ac..f979254a 100644 --- a/internal/service/camel_oil.go +++ b/internal/service/camel_oil.go @@ -98,6 +98,8 @@ type ( OrderDetail(ctx context.Context, req *v1.OrderDetailReq) (res *v1.OrderDetailRes, err error) // ListPrefetchOrder 查询预拉取订单列表 ListPrefetchOrder(ctx context.Context, req *v1.ListPrefetchOrderReq) (res *v1.ListPrefetchOrderRes, err error) + // ExportOrder 导出订单数据为Excel + ExportOrder(ctx context.Context, req *v1.ExportOrderReq) (fileName string, content []byte, err error) // GetPrefetchOrderCapacity 获取当前可用订单容量 GetPrefetchOrderCapacity(ctx context.Context, amount float64) (capacity int, err error) // PrefetchOrderConcurrently 使用所有可用账号并发拉取订单,直到获取到可用订单为止 diff --git a/utility/integration/camel_oil_api/api.go b/utility/integration/camel_oil_api/api.go index ab2261f9..c5d092f3 100644 --- a/utility/integration/camel_oil_api/api.go +++ b/utility/integration/camel_oil_api/api.go @@ -22,6 +22,7 @@ const ( RechargeCardErrorCode = 1 // 卡密错误 RechargeCardErrorToken = 2 // Token 过期/无效 RechargeCardErrorNetwork = 3 // 网络或其他错误 + RechargeCardBannedOneDay = 4 // 账户仅用一天 ) type Client struct { @@ -64,9 +65,6 @@ func (c *Client) SendCaptcha(ctx context.Context, phone string) (bool, error) { } // 记录请求数据 - reqData, _ := json.Marshal(req) - service.CamelOil().SavePrefetchOrderLog(ctx, fmt.Sprintf("发送验证码请求数据 - 数据: %s", string(reqData))) - resp, err := c.Client.ContentJson().Post(ctx, "https://app.bac365.com/camel_wechat_mini_oil_server/sendVerifyMessage", req) if err != nil { glog.Errorf(ctx, "发送验证码请求失败,手机号: %s, 错误: %v", phone, err) @@ -595,6 +593,10 @@ func (c *Client) RechargeCard(ctx context.Context, token, phone, eCardCode strin case "success": glog.Infof(ctx, "卡密绑卡成功,手机号: %s", phone) return RechargeCardSuccess, nil + case "oneDay": + // 输错关于频繁 + glog.Warningf(ctx, "输错关于频繁: %s", phone) + return RechargeCardBannedOneDay, err case "codeError": err = errors.New(respStruct.Message) glog.Warningf(ctx, "卡密错误: %v", err) diff --git a/utility/integration/camel_oil_api/api_test.go b/utility/integration/camel_oil_api/api_test.go index f6ac7f44..ebe16b14 100644 --- a/utility/integration/camel_oil_api/api_test.go +++ b/utility/integration/camel_oil_api/api_test.go @@ -13,7 +13,7 @@ import ( func TestClient_SendCaptcha(t *testing.T) { client := NewClient(t.Context()) - isOk, err := client.SendCaptcha(t.Context(), "19224625031") + isOk, err := client.SendCaptcha(t.Context(), "17862666120") glog.Info(t.Context(), isOk, err) }