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:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
40
internal/controller/camel_oil/camel_oil_v1_export_order.go
Normal file
40
internal/controller/camel_oil/camel_oil_v1_export_order.go
Normal 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
|
||||
}
|
||||
@@ -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)).
|
||||
|
||||
@@ -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()).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 使用所有可用账号并发拉取订单,直到获取到可用订单为止
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user