1. 管理后台 Rock 表格优化:将原「官方发货凭证(FulfillId)」列升级为「发货结果图片」列,提供一键「复制结果图」与「高清预览」Modal,点击即可自动生成凭据大图并直接写入剪贴板(支持在微信/QQ客服聊天框中直接 Ctrl+V 粘贴图片); 2. 商品图片本地缓存与展示修复:从官方 HAR 提取黑色 ROCK 长袖 T 恤与蓝色运动服上衣高清立绘(rock_black_tshirt.webp / rock_blue_sportswear.webp),优化 productImage 自动识别与 Delivery 提货页图片自适应居中,彻底消除灰色占位块; 3. 标杆级发货凭据绘制重构:参考腾讯官方电竞风格设计,重构 640x960 视网膜双倍高清 Canvas 凭据绘制(深海蓝微光渐变背景、顶部微光加粗「兑换成功」、正圆形双层光环立绘、详细角色区服参数对齐、底部翡翠绿发光胶囊按钮),全方位超越 0666 官方粗糙原图; 4. 履约结果字段持久化:后端发货成功后同步持久化角色名、游戏大区与 UID 至 fulfillment_orders 结果报文,确保提货页区服与角色信息完整呈现。
324 lines
9.1 KiB
Go
324 lines
9.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"affiliate_dash/internal/model"
|
|
)
|
|
|
|
// RockProvider 实现 Rock 发货平台履约逻辑
|
|
type RockProvider struct {
|
|
rockService *RockService
|
|
}
|
|
|
|
func NewRockProvider(rockService *RockService) *RockProvider {
|
|
return &RockProvider{
|
|
rockService: rockService,
|
|
}
|
|
}
|
|
|
|
func (p *RockProvider) Name() string {
|
|
return ProviderRock
|
|
}
|
|
|
|
func (p *RockProvider) PrepareDelivery(ctx context.Context, order *model.FulfillmentOrder, openOrder *OpenOrderQuery) (*DeliveryOrderState, error) {
|
|
if p.rockService == nil {
|
|
return nil, errors.New("Rock 发货服务未初始化")
|
|
}
|
|
|
|
cfg, err := p.rockService.GetConfig(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取 Rock 配置失败: %w", err)
|
|
}
|
|
|
|
// 1. 全局软熔断总开关检查 (Kill Switch)
|
|
if !cfg.AutoDeliveryEnabled {
|
|
return &DeliveryOrderState{
|
|
CanShip: false,
|
|
CannotShipReason: "当前该商品自动发货通道维护中,已转交人工客服/打手为您处理",
|
|
Provider: ProviderRock,
|
|
}, nil
|
|
}
|
|
|
|
// 2. 检查可用库存
|
|
// 注意:若当前订单已经分配并锁定了专属 Token (using 或 bound 状态),说明该订单库存已经预占成功,
|
|
// 绝不能因为池子中剩余的空闲库存为 0 而阻断该订单的发货与扫码提货流程!
|
|
hasAssignedToken := false
|
|
orderNo := ""
|
|
if order != nil && strings.TrimSpace(order.OrderNo) != "" {
|
|
orderNo = strings.TrimSpace(order.OrderNo)
|
|
} else if openOrder != nil && strings.TrimSpace(openOrder.OrderNo) != "" {
|
|
orderNo = strings.TrimSpace(openOrder.OrderNo)
|
|
}
|
|
|
|
if orderNo != "" {
|
|
var assignedCount int64
|
|
_ = p.rockService.db.WithContext(ctx).Model(&model.RockToken{}).
|
|
Where("assigned_order_no = ? AND status IN (?, ?)", orderNo, model.RockStatusUsing, model.RockStatusBound).
|
|
Count(&assignedCount).Error
|
|
if assignedCount > 0 {
|
|
hasAssignedToken = true
|
|
}
|
|
}
|
|
|
|
if !hasAssignedToken {
|
|
stats, err := p.rockService.GetStats(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("获取库存失败: %w", err)
|
|
}
|
|
if stats.AvailableStock <= 0 {
|
|
// 若开启了自动补货,立即触发一次补货检查
|
|
go p.rockService.CheckStockAndAutoPurchase(context.Background())
|
|
|
|
return &DeliveryOrderState{
|
|
CanShip: false,
|
|
CannotShipReason: "当前商品库存补充中,已通知管理员自动补货,请稍后刷新或联系客服",
|
|
Provider: ProviderRock,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
sku := ""
|
|
productName := "和平精英道具"
|
|
if order != nil {
|
|
sku = order.ProductSKU
|
|
if order.MerchantProduct != nil && order.MerchantProduct.DisplayName != "" {
|
|
productName = order.MerchantProduct.DisplayName
|
|
}
|
|
}
|
|
if openOrder != nil && openOrder.Product != nil && openOrder.Product.Name != "" {
|
|
productName = openOrder.Product.Name
|
|
}
|
|
|
|
deliveryProd := &DeliveryProduct{
|
|
Name: productName,
|
|
SKU: sku,
|
|
Game: "和平精英",
|
|
}
|
|
|
|
return &DeliveryOrderState{
|
|
CanShip: true,
|
|
Product: deliveryProd,
|
|
Provider: ProviderRock,
|
|
}, nil
|
|
}
|
|
|
|
func (p *RockProvider) Bind(ctx context.Context, order *model.FulfillmentOrder, gameAccount string) (*ProviderBindResult, error) {
|
|
if p.rockService == nil {
|
|
return nil, errors.New("Rock 发货服务未初始化")
|
|
}
|
|
|
|
trimmedUID := strings.TrimSpace(gameAccount)
|
|
if trimmedUID == "" {
|
|
return nil, errors.New("请先填写正确的游戏数字账号(UID)")
|
|
}
|
|
|
|
// 分配或锁定一个 Token 并获取上游活动链接
|
|
token, detail, err := p.rockService.AssignTokenForOrder(ctx, order.OrderNo, trimmedUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
patch := map[string]interface{}{
|
|
"provider": ProviderRock,
|
|
"rock_token_id": token.ID,
|
|
"rock_redeem_code": token.RedeemCode,
|
|
"expected_uid": trimmedUID,
|
|
}
|
|
|
|
bindURL := strings.TrimSpace(detail.BindURL)
|
|
qrURL := detail.QRURL
|
|
if qrURL == "" && bindURL != "" {
|
|
qrURL = qrCodeURL(bindURL)
|
|
}
|
|
|
|
return &ProviderBindResult{
|
|
BindUUID: fmt.Sprintf("%d", token.ID),
|
|
BindURL: bindURL,
|
|
QRURL: qrURL,
|
|
Bound: false,
|
|
ExpectedGameAccount: trimmedUID,
|
|
Message: "请使用手机微信/QQ扫描上方二维码绑定游戏角色",
|
|
Provider: ProviderRock,
|
|
PatchResultData: patch,
|
|
}, nil
|
|
}
|
|
|
|
func (p *RockProvider) CheckBind(ctx context.Context, order *model.FulfillmentOrder, bindUUID string) (*ProviderBindResult, error) {
|
|
if p.rockService == nil {
|
|
return nil, errors.New("Rock 发货服务未初始化")
|
|
}
|
|
|
|
tokenIDUint, err := strconv.ParseUint(strings.TrimSpace(bindUUID), 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("无效的 bind_uuid: %s", bindUUID)
|
|
}
|
|
|
|
expectedUID := resultDataString(order.ResultData, "expected_uid")
|
|
if expectedUID == "" {
|
|
expectedUID = resultDataString(order.RequestData, "game_account")
|
|
}
|
|
if expectedUID == "" {
|
|
expectedUID = resultDataString(order.ResultData, "game_account")
|
|
}
|
|
|
|
res, err := p.rockService.CheckBindingAndVerifyUID(ctx, uint(tokenIDUint), expectedUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
patch := map[string]interface{}{
|
|
"provider": ProviderRock,
|
|
}
|
|
if res.RoleName != "" {
|
|
patch["role_name"] = res.RoleName
|
|
}
|
|
if res.Channel != "" {
|
|
patch["game_channel"] = res.Channel
|
|
}
|
|
if res.RoleID != "" {
|
|
patch["actual_role_id"] = res.RoleID
|
|
}
|
|
|
|
return &ProviderBindResult{
|
|
BindUUID: bindUUID,
|
|
Bound: res.Bound,
|
|
RoleName: res.RoleName,
|
|
GameAccount: res.RoleID,
|
|
GameChannel: res.Channel,
|
|
ExpectedGameAccount: res.ExpectedUID,
|
|
Mismatch: res.Mismatch,
|
|
Message: res.Message,
|
|
Provider: ProviderRock,
|
|
PatchResultData: patch,
|
|
}, nil
|
|
}
|
|
|
|
func (p *RockProvider) Submit(ctx context.Context, order *model.FulfillmentOrder, gameAccount, bindUUID string) (*ProviderSubmitResult, error) {
|
|
if p.rockService == nil {
|
|
return nil, errors.New("Rock 发货服务未初始化")
|
|
}
|
|
|
|
sku := order.ProductSKU
|
|
expectedUID := strings.TrimSpace(gameAccount)
|
|
if expectedUID == "" {
|
|
expectedUID = resultDataString(order.ResultData, "expected_uid")
|
|
}
|
|
|
|
redeemResp, err := p.rockService.RedeemTokenForOrder(ctx, order.OrderNo, sku, expectedUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var token model.RockToken
|
|
_ = p.rockService.db.WithContext(ctx).Where("assigned_order_no = ?", order.OrderNo).Order("id DESC").First(&token).Error
|
|
|
|
fulfillIDStr := ""
|
|
if token.UpstreamFulfillID > 0 {
|
|
fulfillIDStr = fmt.Sprintf("%d", token.UpstreamFulfillID)
|
|
}
|
|
|
|
skinName := redeemResp.Token.RedeemSkin
|
|
if skinName == "" {
|
|
skinName = token.TargetSkin
|
|
}
|
|
currencyName := redeemResp.Token.RedeemCurrency
|
|
if currencyName == "" {
|
|
currencyName = token.TargetCurrency
|
|
}
|
|
|
|
roleName := token.ActualRoleName
|
|
if roleName == "" {
|
|
roleName = resultDataString(order.ResultData, "role_name")
|
|
}
|
|
channel := token.ActualChannel
|
|
if channel == "" {
|
|
channel = resultDataString(order.ResultData, "game_channel")
|
|
}
|
|
|
|
patch := map[string]interface{}{
|
|
"provider": ProviderRock,
|
|
"provider_order_no": fulfillIDStr,
|
|
"fulfill_id": token.UpstreamFulfillID,
|
|
"skin": skinName,
|
|
"currency": currencyName,
|
|
"currency_quantity": token.TargetCurrencyQuantity,
|
|
"role_name": roleName,
|
|
"game_channel": channel,
|
|
"game_uid": expectedUID,
|
|
}
|
|
|
|
return &ProviderSubmitResult{
|
|
ProviderOrderNo: fulfillIDStr,
|
|
NextStatus: model.OrderStatusDelivered,
|
|
Message: "发货成功!道具及附赠货币已发放至游戏,请登录游戏邮件查收",
|
|
GameAccount: map[string]interface{}{
|
|
"role_name": roleName,
|
|
"game_channel": channel,
|
|
"role_id": expectedUID,
|
|
},
|
|
PatchResultData: patch,
|
|
}, nil
|
|
}
|
|
|
|
func (p *RockProvider) SyncStatus(ctx context.Context, order *model.FulfillmentOrder) (*ProviderSyncResult, error) {
|
|
if p.rockService == nil {
|
|
return nil, errors.New("Rock 发货服务未初始化")
|
|
}
|
|
|
|
var token model.RockToken
|
|
err := p.rockService.db.WithContext(ctx).
|
|
Where("assigned_order_no = ?", order.OrderNo).
|
|
Order("id DESC").
|
|
First(&token).Error
|
|
if err != nil {
|
|
return &ProviderSyncResult{
|
|
Status: "",
|
|
}, nil
|
|
}
|
|
|
|
switch token.Status {
|
|
case model.RockStatusRedeemed:
|
|
return &ProviderSyncResult{
|
|
Status: "success",
|
|
ProviderOrderNo: fmt.Sprintf("%d", token.UpstreamFulfillID),
|
|
GameUID: token.ActualRoleID,
|
|
RoleName: token.ActualRoleName,
|
|
RoleArea: token.ActualChannel,
|
|
}, nil
|
|
case model.RockStatusMismatch:
|
|
return &ProviderSyncResult{
|
|
Status: "failed",
|
|
FailReason: token.MismatchReason,
|
|
}, nil
|
|
default:
|
|
return &ProviderSyncResult{
|
|
Status: "delivering",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (p *RockProvider) Cancel(ctx context.Context, order *model.FulfillmentOrder) error {
|
|
if p.rockService == nil {
|
|
return nil
|
|
}
|
|
|
|
// 若订单尚未扫码绑定成功,将处于 using 状态的 Token 安全重置回 available
|
|
var token model.RockToken
|
|
err := p.rockService.db.WithContext(ctx).
|
|
Where("assigned_order_no = ? AND status = ?", order.OrderNo, model.RockStatusUsing).
|
|
First(&token).Error
|
|
if err == nil {
|
|
_ = p.rockService.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
|
|
"status": model.RockStatusAvailable,
|
|
"assigned_order_no": "",
|
|
"expected_uid": "",
|
|
}).Error
|
|
}
|
|
return nil
|
|
}
|