feat:跳转上游不限状态显示
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
@@ -27,15 +28,25 @@ func (s *RockService) TokenPackageLink(ctx context.Context, tokenID uint) (*Toke
|
||||
if token.PackageID == nil || *token.PackageID <= 0 {
|
||||
return nil, errors.New("该 Token 没有关联上游打包记录,无法跳转上游")
|
||||
}
|
||||
shareCode := strings.TrimSpace(token.PackageShareCode)
|
||||
if shareCode == "" {
|
||||
return nil, errors.New("该 Token 缺少上游包分享码(历史包不支持定位),无法跳转上游")
|
||||
}
|
||||
|
||||
cfg, err := s.GetConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
shareCode := strings.TrimSpace(token.PackageShareCode)
|
||||
if shareCode == "" {
|
||||
shareCode, err = s.resolvePackageShareCode(ctx, *token.PackageID, cfg.BearerToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 反查成功后回填同包 Token,后续点击无需再请求上游。
|
||||
if updateErr := s.db.WithContext(ctx).Model(&model.RockToken{}).
|
||||
Where("package_id = ? AND package_share_code = ''", *token.PackageID).
|
||||
Update("package_share_code", shareCode).Error; updateErr != nil {
|
||||
log.Printf("[RockService] 回填 package_share_code 失败 package_id=%d: %v", *token.PackageID, updateErr)
|
||||
}
|
||||
}
|
||||
|
||||
base := strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
|
||||
if base == "" {
|
||||
base = model.RockDefaultBaseURL
|
||||
@@ -47,3 +58,45 @@ func (s *RockService) TokenPackageLink(ctx context.Context, tokenID uint) (*Toke
|
||||
URL: fmt.Sprintf("%s/#/package/%s", base, url.PathEscape(shareCode)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolvePackageShareCode 在本地没有分享码时,按 package_id 从上游包列表反查。
|
||||
// 历史 Token 只落了 package_id,package_share_code 是后续版本才开始保存的。
|
||||
func (s *RockService) resolvePackageShareCode(ctx context.Context, packageID int64, bearerToken string) (string, error) {
|
||||
if packageID <= 0 {
|
||||
return "", errors.New("该 Token 没有关联上游打包记录,无法跳转上游")
|
||||
}
|
||||
bearer := strings.TrimSpace(bearerToken)
|
||||
if bearer == "" {
|
||||
return "", errors.New("该 Token 缺少上游包分享码,且未配置 0666.pro Bearer Token,无法反查上游包")
|
||||
}
|
||||
// 最多翻 20 页,防止上游分页异常导致死循环。
|
||||
for page := 1; page <= 20; page++ {
|
||||
resp, err := s.client.ListPackages(ctx, bearer, page, 100)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("反查上游包分享码失败: %w", err)
|
||||
}
|
||||
if resp == nil {
|
||||
break
|
||||
}
|
||||
for _, pkg := range resp.Packages {
|
||||
if pkg.ID != packageID {
|
||||
continue
|
||||
}
|
||||
code := strings.TrimSpace(pkg.ShareCode)
|
||||
if code == "" {
|
||||
return "", errors.New("上游该包未返回分享码,无法跳转上游")
|
||||
}
|
||||
return code, nil
|
||||
}
|
||||
if resp.Pagination.TotalPages > 0 {
|
||||
if page >= resp.Pagination.TotalPages {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(resp.Packages) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return "", errors.New("上游未找到该 Token 所属打包记录(可能已被清理),无法跳转上游")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -57,7 +59,7 @@ func TestTokenPackageLink_RejectsWithoutPackage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenPackageLink_RejectsMissingShareCode(t *testing.T) {
|
||||
func TestTokenPackageLink_RejectsMissingShareCodeWithoutBearer(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
svc := NewRockService(db, NewRockClient("https://example.com"))
|
||||
|
||||
@@ -73,8 +75,93 @@ func TestTokenPackageLink_RejectsMissingShareCode(t *testing.T) {
|
||||
t.Fatalf("创建 Token 失败: %v", err)
|
||||
}
|
||||
|
||||
if _, err := svc.TokenPackageLink(context.Background(), token.ID); err == nil || !strings.Contains(err.Error(), "缺少上游包分享码") {
|
||||
t.Fatalf("缺少分享码应被拒绝,实际 err=%v", err)
|
||||
if _, err := svc.TokenPackageLink(context.Background(), token.ID); err == nil || !strings.Contains(err.Error(), "无法反查上游包") {
|
||||
t.Fatalf("缺少 Bearer Token 时应拒绝反查,实际 err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenPackageLink_ResolvesAndBackfillsShareCode(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path != "/api/user/packages" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"packages": []map[string]interface{}{
|
||||
{"id": 1509, "share_code": "share-other"},
|
||||
{"id": 1508, "share_code": "share-resolved"},
|
||||
},
|
||||
"pagination": map[string]interface{}{"page": 1, "pageSize": 100, "total": 2, "totalPages": 1},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
svc := NewRockService(db, NewRockClient(server.URL))
|
||||
setRockTestBearer(t, svc, "bearer-test")
|
||||
|
||||
packageID := int64(1508)
|
||||
tokens := []model.RockToken{
|
||||
{UpstreamTokenID: "9103", RedeemCode: "CODE-NOSHARE", Color: model.RockColorBlack, Status: model.RockStatusMismatch, PackageID: &packageID},
|
||||
{UpstreamTokenID: "9104", RedeemCode: "CODE-NOSHARE-2", Color: model.RockColorBlack, Status: model.RockStatusAvailable, PackageID: &packageID},
|
||||
}
|
||||
for i := range tokens {
|
||||
if err := db.Create(&tokens[i]).Error; err != nil {
|
||||
t.Fatalf("创建 Token 失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := svc.TokenPackageLink(context.Background(), tokens[0].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("反查分享码失败: %v", err)
|
||||
}
|
||||
if res.ShareCode != "share-resolved" || !strings.HasSuffix(res.URL, "/#/package/share-resolved") {
|
||||
t.Fatalf("反查结果不符: %+v", res)
|
||||
}
|
||||
|
||||
var saved []model.RockToken
|
||||
if err := db.Where("package_id = ?", packageID).Find(&saved).Error; err != nil {
|
||||
t.Fatalf("查询 Token 失败: %v", err)
|
||||
}
|
||||
if len(saved) != len(tokens) {
|
||||
t.Fatalf("Token 数量不符: %d", len(saved))
|
||||
}
|
||||
for _, row := range saved {
|
||||
if row.PackageShareCode != "share-resolved" {
|
||||
t.Fatalf("同包 Token 未回填: id=%d code=%q", row.ID, row.PackageShareCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenPackageLink_RejectsWhenUpstreamPackageMissing(t *testing.T) {
|
||||
db := newRockTestDB(t)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"packages": []map[string]interface{}{},
|
||||
"pagination": map[string]interface{}{"page": 1, "pageSize": 100, "total": 0, "totalPages": 1},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
svc := NewRockService(db, NewRockClient(server.URL))
|
||||
setRockTestBearer(t, svc, "bearer-test")
|
||||
|
||||
packageID := int64(1599)
|
||||
token := model.RockToken{
|
||||
UpstreamTokenID: "9105",
|
||||
RedeemCode: "CODE-GONE",
|
||||
Color: model.RockColorBlack,
|
||||
Status: model.RockStatusMismatch,
|
||||
PackageID: &packageID,
|
||||
}
|
||||
if err := db.Create(&token).Error; err != nil {
|
||||
t.Fatalf("创建 Token 失败: %v", err)
|
||||
}
|
||||
|
||||
if _, err := svc.TokenPackageLink(context.Background(), token.ID); err == nil || !strings.Contains(err.Error(), "上游未找到") {
|
||||
t.Fatalf("上游无该包时应返回明确错误,实际 err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -451,18 +451,32 @@ export function buildTokenColumns({
|
||||
刷新
|
||||
</Button>
|
||||
)
|
||||
if (record.status === 'mismatch') {
|
||||
return (
|
||||
<Space direction="vertical" size={4}>
|
||||
{refreshButton}
|
||||
const upstreamDisabled = !record.package_id
|
||||
const upstreamTip = upstreamDisabled
|
||||
? '该 Token 未关联上游打包记录,无法跳转上游'
|
||||
: record.package_share_code
|
||||
? ''
|
||||
: '本地暂无包分享码,点击时会自动向上游反查'
|
||||
const upstreamButton = (
|
||||
<Tooltip title={upstreamTip || undefined}>
|
||||
<span style={{ display: 'inline-block' }}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ExportOutlined />}
|
||||
disabled={upstreamDisabled}
|
||||
loading={openingUpstreamId === record.id}
|
||||
onClick={() => onOpenUpstream(record.id)}
|
||||
>
|
||||
跳转上游
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
if (record.status === 'mismatch') {
|
||||
return (
|
||||
<Space direction="vertical" size={4}>
|
||||
{refreshButton}
|
||||
{upstreamButton}
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
@@ -507,6 +521,7 @@ export function buildTokenColumns({
|
||||
return (
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>已失效作废</Text>
|
||||
{upstreamButton}
|
||||
<Popconfirm
|
||||
title="确定强制恢复为可用库存?"
|
||||
description="警告:该链接曾标记为已上报失效。若上游已作废该卡密,重新放回库存将导致下一单提货失败!请确保卡密真实可用。"
|
||||
@@ -527,6 +542,7 @@ export function buildTokenColumns({
|
||||
return (
|
||||
<Space direction="vertical" size={4}>
|
||||
{refreshButton}
|
||||
{upstreamButton}
|
||||
{canRelease && (
|
||||
<Popconfirm
|
||||
title="释放这条未绑定的提货链接?"
|
||||
@@ -544,9 +560,18 @@ export function buildTokenColumns({
|
||||
)
|
||||
}
|
||||
if (record.status === 'bound') {
|
||||
return refreshButton
|
||||
return (
|
||||
<Space direction="vertical" size={4}>
|
||||
{refreshButton}
|
||||
{upstreamButton}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
return <Text type="secondary">-</Text>
|
||||
return (
|
||||
<Space direction="vertical" size={4}>
|
||||
{upstreamButton}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user