Files
kami_backend/internal/logic/camel_oil/token.go
danial 85b552eec3 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
2025-12-11 20:13:52 +08:00

581 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package camel_oil
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/shopspring/decimal"
"kami/internal/model"
"kami/utility/utils"
"kami/internal/consts"
"kami/internal/dao"
"kami/internal/model/do"
"kami/internal/model/entity"
"kami/internal/service"
"kami/utility/config"
"kami/utility/integration/camel_oil_api"
)
// ====================================================================================
// Token 管理相关方法
// ====================================================================================
// CreateToken 创建 Token
func (s *sCamelOil) CreateToken(ctx context.Context, req *model.CamelOilTokenCreateInput) (tokenId int64, err error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
// 获取当前用户信息
_, userInfo, err := service.SysUser().GetUserFromToken(ctx)
if err != nil {
return 0, gerror.Wrap(err, "获取用户信息失败")
}
var userId string
if userInfo != nil && userInfo.Id != "" {
userId = userInfo.Id
} else {
return 0, gerror.NewCode(gcode.CodeNotAuthorized, "权限不足")
}
// 将 float64 转换为 decimal.Decimal 存储到数据库
rechargeLimitAmountDecimal := decimal.NewFromFloat(req.RechargeLimitAmount)
isOk, err := camel_oil_api.NewClient(ctx).SendCaptcha(ctx, req.Phone)
if !isOk {
return 0, gerror.NewCode(gcode.CodeBusinessValidationFailed, "验证码发送失败")
}
result, err := m.Insert(&do.V1CamelOilToken{
UserId: userId,
Name: req.Name,
Phone: req.Phone,
Status: consts.CamelOilTokenStatusCodeSent,
BindCount: 0,
TotalBindAmount: decimal.Zero,
TotalRechargeAmount: decimal.Zero,
RechargeLimitAmount: rechargeLimitAmountDecimal,
RechargeLimitCount: req.RechargeLimitCount,
Remark: req.Remark,
})
if err != nil {
return 0, gerror.Wrap(err, "创建 Token失败")
}
tokenId, _ = result.LastInsertId()
return tokenId, nil
}
// GetTokenInfo 获取 Token 信息
func (s *sCamelOil) GetTokenInfo(ctx context.Context, req *model.CamelOilTokenQueryInput) (token *entity.V1CamelOilToken, err error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1CamelOilToken.Columns().Id, req.TokenId).Scan(&token)
if err != nil {
return nil, gerror.Wrap(err, "查询Token信息失败")
}
if token == nil {
return nil, gerror.New("Token不存在")
}
// 权限检查普通用户只能查看自己的token
needAuth, userInfo, err := service.SysUser().GetUserFromToken(ctx)
if err != nil {
return nil, gerror.Wrap(err, "获取用户信息失败")
}
// 如果是普通用户且token不属于当前用户拒绝访问
if needAuth && userInfo != nil && userInfo.Id != "" && token.UserId != userInfo.Id {
return nil, gerror.NewCode(gcode.CodeNotAuthorized, "无权访问此Token")
}
return token, nil
}
// ListTokens 列出所有可用的 Token
func (s *sCamelOil) ListTokens(ctx context.Context) (tokens []*entity.V1CamelOilToken, err error) {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1CamelOilToken.Columns().Status, consts.CamelOilTokenStatusAvailable).
OrderAsc(dao.V1CamelOilToken.Columns().LastUsedAt).
OrderAsc(dao.V1CamelOilToken.Columns().LastBindAt).
Scan(&tokens)
if err != nil {
return nil, gerror.Wrap(err, "查询Token列表失败")
}
return tokens, nil
}
// ListTokensWithPagination 列出所有可用的 Token支持分页和查询条件
func (s *sCamelOil) ListTokensWithPagination(ctx context.Context, req *model.CamelOilTokenListInput) (tokens []*entity.V1CamelOilToken, total int, err error) {
query := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
// 获取当前用户信息
needAuth, userInfo, _ := service.SysUser().GetUserFromToken(ctx)
// 如果是普通用户非管理员只能查看自己的token
if needAuth && userInfo != nil && userInfo.Id != "" {
query = query.Where(dao.V1CamelOilToken.Columns().UserId, userInfo.Id)
}
// 管理员或不需要认证的情况可以查看所有token包括user_id为空的token
// 添加查询条件
if req.Name != "" {
query = query.WhereLike(dao.V1CamelOilToken.Columns().Name, utils.OrmLike(req.Name))
}
if req.Status > 0 {
query = query.Where(dao.V1CamelOilToken.Columns().Status, req.Status)
}
// 获取总数
totalCount, err := query.Count()
if err != nil {
return nil, 0, gerror.Wrap(err, "查询Token总数失败")
}
// 分页查询
err = query.
Offset((req.Current - 1) * req.PageSize).
Limit(req.PageSize).
OrderDesc(dao.V1CamelOilToken.Columns().CreatedAt).
Scan(&tokens)
if err != nil {
return nil, 0, gerror.Wrap(err, "查询Token列表失败")
}
return tokens, totalCount, nil
}
// UpdateTokenInfo 修改 Token 基本信息(不包括 tokenValue
func (s *sCamelOil) UpdateTokenInfo(ctx context.Context, req *model.CamelOilTokenUpdateInput) error {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
// 权限检查获取token信息验证所有权
var token *entity.V1CamelOilToken
err := m.Where(dao.V1CamelOilToken.Columns().Id, req.TokenId).Scan(&token)
if err != nil {
return gerror.Wrap(err, "查询Token信息失败")
}
if token == nil {
return gerror.New("Token不存在")
}
// 权限检查普通用户只能修改自己的token
needAuth, userInfo, err := service.SysUser().GetUserFromToken(ctx)
if err != nil {
return gerror.Wrap(err, "获取用户信息失败")
}
// 如果是普通用户且token不属于当前用户拒绝修改
if needAuth && userInfo != nil && userInfo.Id != "" && token.UserId != userInfo.Id {
return gerror.NewCode(gcode.CodeNotAuthorized, "无权修改此Token")
}
// 将 float64 转换为 decimal.Decimal 存储到数据库
rechargeLimitAmountDecimal := decimal.NewFromFloat(req.RechargeLimitAmount)
_, err = m.Where(dao.V1CamelOilToken.Columns().Id, req.TokenId).Update(&do.V1CamelOilToken{
Name: req.Name,
Remark: req.Remark,
RechargeLimitAmount: rechargeLimitAmountDecimal,
RechargeLimitCount: req.RechargeLimitCount,
})
if err != nil {
return gerror.Wrap(err, "修改Token信息失败")
}
glog.Infof(ctx, "Token信息修改成功: tokenId=%d, tokenName=%s", req.TokenId, req.Name)
return nil
}
// DeleteToken 删除 Token软删除
func (s *sCamelOil) DeleteToken(ctx context.Context, req *model.CamelOilTokenDeleteInput) error {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
// 权限检查获取token信息验证所有权
var token *entity.V1CamelOilToken
err := m.Where(dao.V1CamelOilToken.Columns().Id, req.TokenId).Scan(&token)
if err != nil {
return gerror.Wrap(err, "查询Token信息失败")
}
if token == nil {
return gerror.New("Token不存在")
}
// 权限检查普通用户只能删除自己的token
needAuth, userInfo, err := service.SysUser().GetUserFromToken(ctx)
if err != nil {
return gerror.Wrap(err, "获取用户信息失败")
}
// 如果是普通用户且token不属于当前用户拒绝删除
if needAuth && userInfo != nil && userInfo.Id != "" && token.UserId != userInfo.Id {
return gerror.NewCode(gcode.CodeNotAuthorized, "无权删除此Token")
}
_, err = m.Where(dao.V1CamelOilToken.Columns().Id, req.TokenId).Delete()
if err != nil {
return gerror.Wrap(err, "删除Token失败")
}
glog.Infof(ctx, "Token删除成功: tokenId=%d", req.TokenId)
return nil
}
// UpdateTokenStatus 更新 Token 状态并记录日志
func (s *sCamelOil) UpdateTokenStatus(ctx context.Context, req *model.CamelOilTokenStatusUpdateInput) error {
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
// 获取当前 Token 信息
var token *entity.V1CamelOilToken
err := m.Where(dao.V1CamelOilToken.Columns().Id, req.TokenId).Scan(&token)
if err != nil {
return gerror.Wrap(err, "查询Token失败")
}
if token == nil {
return gerror.New("Token不存在")
}
oldStatus := consts.CamelOilTokenStatus(token.Status)
// 如果状态没有变化,则不更新
if oldStatus == req.NewStatus {
return nil
}
// 更新 Token 状态
_, err = m.Where(dao.V1CamelOilToken.Columns().Id, req.TokenId).Update(&do.V1CamelOilToken{
Status: int(req.NewStatus),
Remark: req.Remark,
})
if err != nil {
return gerror.Wrap(err, "更新Token状态失败")
}
glog.Infof(ctx, "Token状态更新成功: tokenId=%d, 原状态=%s, 新状态=%s, 备注=%s", req.TokenId, consts.CamelOilTokenStatusText[oldStatus], consts.CamelOilTokenStatusText[req.NewStatus], req.Remark)
return nil
}
// ====================================================================================
// 卡密绑定相关方法
// ====================================================================================
// BindCardToToken 绑定卡密到 Token使用轮询算法选择 Token
func (s *sCamelOil) BindCardToToken(ctx context.Context, req *model.CamelOilCardBindInput) (bindingId int64, err error) {
orderId := req.OrderId
cardNumber := req.CardNumber
cardPassword := req.CardPassword
amount := req.Amount
// 1. 获取订单信息
var order *entity.V1CamelOilOrder
err = dao.V1CamelOilOrder.Ctx(ctx).DB(config.GetDatabaseV1()).
Where(dao.V1CamelOilOrder.Columns().Id, orderId).
Scan(&order)
if err != nil {
return 0, gerror.Wrap(err, "查询订单失败")
}
if order == nil {
return 0, gerror.New("订单不存在")
}
// 2. 获取所有可用的 Token
tokens, err := s.ListTokens(ctx)
if err != nil {
return 0, gerror.Wrap(err, "查询Token列表失败")
}
if len(tokens) == 0 {
return 0, gerror.New("没有可用的Token")
}
// 3. 使用轮询算法选择 Token选择绑定金额最少的 Token
var selectedToken *entity.V1CamelOilToken
minAmount := tokens[0].TotalBindAmount
selectedToken = tokens[0]
for _, token := range tokens {
if token.TotalBindAmount.Cmp(minAmount) < 0 {
minAmount = token.TotalBindAmount
selectedToken = token
}
}
// 4.2 调用绑卡接口(使用选中 Token 的 LoginToken
rechargeErrType, rechargeErr := camel_oil_api.NewClient(ctx).RechargeCard(ctx, selectedToken.LoginToken, selectedToken.Phone, cardPassword)
if rechargeErr != nil {
switch rechargeErrType {
case camel_oil_api.RechargeCardErrorCode:
// 卡密错误:标记订单为绑定失败
glog.Warningf(ctx, "卡密错误: %v", rechargeErr)
// 调用已实现的方法更新订单状态
_ = 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)
// 调用已实现的方法更新 Token 状态
_ = s.UpdateTokenStatus(ctx, &model.CamelOilTokenStatusUpdateInput{
TokenId: selectedToken.Id,
NewStatus: consts.CamelOilTokenStatusExpired,
Remark: "Token已过期",
})
return 0, gerror.Wrap(rechargeErr, "Token过期需要重新登录")
default:
// 网络或其他错误:不更新状态,由定时任务重试
glog.Errorf(ctx, "绑卡失败(网络或其他错误): %v", rechargeErr)
return 0, gerror.Wrap(rechargeErr, "绑卡操作失败,稍后重试")
}
}
// 5. 创建绑定记录
bindingResult, err := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1()).
Insert(&do.V1CamelOilCardBinding{
TokenId: selectedToken.Id,
OrderId: orderId,
CardNumber: cardNumber,
CardPassword: cardPassword,
Amount: amount,
})
if err != nil {
return 0, gerror.Wrap(err, "创建绑定记录失败")
}
bindingId, _ = bindingResult.LastInsertId()
_, _ = dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1()).Where(dao.V1CamelOilToken.Columns().Id, selectedToken.Id).
Data(dao.V1CamelOilToken.Columns().TotalBindAmount, &gdb.Counter{
Field: dao.V1CamelOilToken.Columns().TotalBindAmount,
Value: gconv.Float64(amount),
}).Data(dao.V1CamelOilToken.Columns().BindCount, &gdb.Counter{
Field: dao.V1CamelOilToken.Columns().BindCount,
Value: gconv.Float64(amount),
}).Data(dao.V1CamelOilToken.Columns().LastBindAt, gtime.Now()).Data(dao.V1CamelOilToken.Columns().LastUsedAt, gtime.Now()).Update()
//给商户加宽
_, _ = service.SysUserPayment().Consumption(ctx, &model.SysUserPaymentRechargeOrConsumeInput{
UserId: selectedToken.UserId,
OrderNo: gconv.String(orderId),
Amount: amount,
Remark: "核销扣款",
TransactionType: consts.UserPaymentRecordStatusConsume,
Category: consts.CardRedeemAccountCategoryCamelOil,
}, nil)
glog.Infof(ctx, "卡密绑定成功: bindingId=%d, tokenId=%d, 订单ID=%d, 绑定金额=%.2f",
bindingId, selectedToken.Id, orderId, amount.InexactFloat64())
return bindingId, nil
}
// GetCardBindingInfo 获取卡密绑定信息
func (s *sCamelOil) GetCardBindingInfo(ctx context.Context, bindingId int64) (binding *entity.V1CamelOilCardBinding, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1CamelOilCardBinding.Columns().Id, bindingId).Scan(&binding)
if err != nil {
return nil, gerror.Wrap(err, "查询绑定信息失败")
}
if binding == nil {
return nil, gerror.New("绑定记录不存在")
}
return binding, nil
}
// GetCardBindingByOrder 获取订单绑定的卡密信息
func (s *sCamelOil) GetCardBindingByOrder(ctx context.Context, orderId int64) (binding *entity.V1CamelOilCardBinding, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
err = m.Where(dao.V1CamelOilCardBinding.Columns().OrderId, orderId).Scan(&binding)
if err != nil {
return nil, gerror.Wrap(err, "查询绑定信息失败")
}
return binding, nil
}
// GetCardBindingsByToken 根据 tokenId 查询绑定的卡密信息
func (s *sCamelOil) GetCardBindingsByToken(ctx context.Context, req *model.CamelOilCardListInput) (bindings []*entity.V1CamelOilCardBinding, total int, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
// 权限检查获取token信息验证所有权
token, err := s.GetTokenInfo(ctx, &model.CamelOilTokenQueryInput{TokenId: req.TokenId})
if err != nil {
return nil, 0, err // GetTokenInfo已经包含权限检查
}
if token == nil {
return nil, 0, gerror.New("Token不存在")
}
count, err := m.Where(dao.V1CamelOilCardBinding.Columns().TokenId, req.TokenId).Count()
if err != nil {
return nil, 0, gerror.Wrap(err, "查询绑定记录计数失败")
}
err = m.Where(dao.V1CamelOilCardBinding.Columns().TokenId, req.TokenId).
Page(req.Current, req.PageSize).
Scan(&bindings)
if err != nil {
return nil, 0, gerror.Wrap(err, "查询绑定记录失败")
}
return bindings, int(count), nil
}
// GetTokenBindingStats 获取 Token 的绑定统计
func (s *sCamelOil) GetTokenBindingStats(ctx context.Context, tokenId int64) (bindCount int, totalAmount decimal.Decimal, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
var stats struct {
BindCount int
TotalAmount decimal.Decimal
}
err = m.Where(dao.V1CamelOilCardBinding.Columns().TokenId, tokenId).
Fields("COUNT(*) as bind_count, SUM(amount) as total_amount").
Scan(&stats)
if err != nil {
return 0, decimal.Zero, gerror.Wrap(err, "查询Token绑定统计失败")
}
return stats.BindCount, stats.TotalAmount, nil
}
// CalculateTotalBindingAmount 计算所有 Token 的累计绑定金额
func (s *sCamelOil) CalculateTotalBindingAmount(ctx context.Context) (totalAmount decimal.Decimal, err error) {
m := dao.V1CamelOilCardBinding.Ctx(ctx).DB(config.GetDatabaseV1())
var result struct {
Total decimal.Decimal
}
err = m.Fields("SUM(amount) as total").Scan(&result)
if err != nil {
return decimal.Zero, gerror.Wrap(err, "计算累计绑定金额失败")
}
return result.Total, nil
}
// InputVerificationCode 输入验证码
func (s *sCamelOil) InputVerificationCode(ctx context.Context, req *model.CamelOilTokenLoginInput) (string, error) {
// 获取 Token 信息
token, err := s.GetTokenInfo(ctx, &model.CamelOilTokenQueryInput{TokenId: req.TokenId})
if err != nil {
return "", gerror.Wrap(err, "查询Token信息失败")
}
if token == nil {
return "", gerror.New("Token不存在")
}
// 检查 Token 状态,只有验证码已发送或验证失败的 Token 才能输入验证码
if token.Status != int(consts.CamelOilTokenStatusCodeSent) && token.Status != int(consts.CamelOilTokenStatusVerificationFailed) {
return "", gerror.New("Token状态不允许输入验证码")
}
client := camel_oil_api.NewClient(ctx)
// 使用验证码登录
loginToken, err := client.LoginWithCaptcha(ctx, token.Phone, req.Code)
if err != nil {
return "", gerror.Wrap(err, "验证码验证失败")
}
if loginToken == "" {
return "", gerror.New("验证码验证失败")
}
// 更新 Token 信息
m := dao.V1CamelOilToken.Ctx(ctx).DB(config.GetDatabaseV1())
_, err = m.Where(dao.V1CamelOilToken.Columns().Id, req.TokenId).Update(&do.V1CamelOilToken{
LoginToken: loginToken,
LoginTokenExpiresAt: gtime.Now().Add(gtime.D * 30), // 30天后过期
LastLoginAt: gtime.Now(),
Status: int(consts.CamelOilTokenStatusAvailable), // 可用状态
})
if err != nil {
return "", gerror.Wrap(err, "更新Token登录信息失败")
}
glog.Infof(ctx, "验证码验证成功Token登录: tokenId=%d, phone=%s", req.TokenId, token.Phone)
return loginToken, nil
}
// ResendVerificationCode 重新发送验证码
func (s *sCamelOil) ResendVerificationCode(ctx context.Context, req *model.CamelOilTokenResendCodeInput) error {
// 获取 Token 信息
token, err := s.GetTokenInfo(ctx, &model.CamelOilTokenQueryInput{TokenId: req.TokenId})
if err != nil {
return gerror.Wrap(err, "查询Token信息失败")
}
if token == nil {
return gerror.New("Token不存在")
}
// 检查 Token 状态,只有特定状态的 Token 才能重新发送验证码
allowedStatuses := []int{
int(consts.CamelOilTokenStatusPendingVerification), // 待验证码
int(consts.CamelOilTokenStatusCodeSent), // 验证码已发送
int(consts.CamelOilTokenStatusVerificationFailed), // 验证码验证失败
int(consts.CamelOilTokenStatusLoginFailed), // 登录失败
}
statusAllowed := false
for _, status := range allowedStatuses {
if token.Status == status {
statusAllowed = true
break
}
}
if !statusAllowed {
return gerror.New("Token状态不允许重新发送验证码")
}
client := camel_oil_api.NewClient(ctx)
// 发送验证码
_, err = client.SendCaptcha(ctx, token.Phone)
if err != nil {
// 更新状态为登录失败
_ = s.UpdateTokenStatus(ctx, &model.CamelOilTokenStatusUpdateInput{
TokenId: req.TokenId,
NewStatus: consts.CamelOilTokenStatusLoginFailed,
Remark: "发送验证码失败",
})
return gerror.Wrap(err, "发送验证码失败")
}
// 更新 Token 状态为验证码已发送
err = s.UpdateTokenStatus(ctx, &model.CamelOilTokenStatusUpdateInput{
TokenId: req.TokenId,
NewStatus: consts.CamelOilTokenStatusCodeSent,
Remark: "重新发送验证码成功",
})
if err != nil {
return gerror.Wrap(err, "更新Token状态失败")
}
glog.Infof(ctx, "验证码重新发送成功: tokenId=%d, phone=%s", req.TokenId, token.Phone)
return nil
}