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
This commit is contained in:
danial
2025-12-11 20:13:52 +08:00
parent 75a032019a
commit 85b552eec3
11 changed files with 249 additions and 14 deletions

View File

@@ -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)

View File

@@ -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 {
}

View File

@@ -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{

View File

@@ -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
}

View File

@@ -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) {
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)).

View File

@@ -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()).

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 使用所有可用账号并发拉取订单,直到获取到可用订单为止

View File

@@ -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)

View File

@@ -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)
}