feat: 迁移游戏商务绑定审核流程和列表
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -24,6 +25,16 @@ type businessHTTPData struct{ db *gorm.DB }
|
||||
|
||||
func (d businessHTTPData) DB() *gorm.DB { return d.db }
|
||||
|
||||
type httpPlatformAccounts struct{}
|
||||
|
||||
func (httpPlatformAccounts) FindByAccount(context.Context, string, string) (*businessbiz.PlatformAccount, error) {
|
||||
return &businessbiz.PlatformAccount{ID: 77, Status: 1}, nil
|
||||
}
|
||||
|
||||
type httpPlatformPasswords struct{}
|
||||
|
||||
func (httpPlatformPasswords) Verify(string, string) (bool, error) { return true, nil }
|
||||
|
||||
func TestBusinessHTTPListWithSystemOnlyAndCompleteDatabase(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "http.db")), &gorm.Config{})
|
||||
if err != nil {
|
||||
@@ -40,7 +51,8 @@ func TestBusinessHTTPListWithSystemOnlyAndCompleteDatabase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for _, statement := range []string{
|
||||
"INSERT INTO sys_users(id,uuid,username,authority_id,level,id_path) VALUES (1,'admin','admin',888,0,'0'),(2,'child','child',901,1,'0'),(3,'empty','empty',901,1,'0')",
|
||||
"INSERT INTO sys_users(id,uuid,username,authority_id,level,id_path) VALUES (1,'admin','admin',888,0,'0'),(2,'child','child',901,1,'0'),(3,'empty','empty',903,3,'0,2')",
|
||||
"INSERT INTO sys_authorities(authority_id,authority_name) VALUES (901,'一级商务')",
|
||||
"INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,status) VALUES (2,'lx',9,'0')",
|
||||
} {
|
||||
if err := db.Exec(statement).Error; err != nil {
|
||||
@@ -48,15 +60,21 @@ func TestBusinessHTTPListWithSystemOnlyAndCompleteDatabase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
provider := businessHTTPData{db}
|
||||
accounts := businessservice.NewAccountService(businessbiz.NewAccountUsecase(businessdata.NewAccountRepo(provider)))
|
||||
accountUsecase := businessbiz.NewAccountUsecase(businessdata.NewAccountRepo(provider))
|
||||
accounts := businessservice.NewAccountService(accountUsecase)
|
||||
bindings := businessservice.NewBindingService(businessbiz.NewBindingUsecase(businessdata.NewBindingRepo(provider), accountUsecase, httpPlatformAccounts{}, httpPlatformPasswords{}))
|
||||
prefs := businessservice.NewUserPreferenceService(businessbiz.NewUserPreferenceUsecase(businessdata.NewUserPreferenceRepo(provider)))
|
||||
engine := gin.New()
|
||||
// 此契约测试固定已认证身份;真实登录另通过运行实例验证。
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Request = c.Request.WithContext(system.NewActorContext(c.Request.Context(), system.Actor{UserID: 1, AuthorityID: 888}))
|
||||
actor := system.Actor{UserID: 1, AuthorityID: 888}
|
||||
if c.GetHeader("X-Test-Actor") == "3" {
|
||||
actor = system.Actor{UserID: 3, AuthorityID: 903}
|
||||
}
|
||||
c.Request = c.Request.WithContext(system.NewActorContext(c.Request.Context(), actor))
|
||||
c.Next()
|
||||
})
|
||||
router.RegisterBusiness(engine.Group(""), handler.NewBusiness(prefs, accounts))
|
||||
router.RegisterBusiness(engine.Group(""), handler.NewBusiness(prefs, accounts, bindings))
|
||||
server := httptest.NewServer(engine)
|
||||
defer server.Close()
|
||||
requestList := func() map[string]any {
|
||||
@@ -99,4 +117,47 @@ func TestBusinessHTTPListWithSystemOnlyAndCompleteDatabase(t *testing.T) {
|
||||
if child["promoteCount"] != float64(1) {
|
||||
t.Fatalf("完整数据应恢复真实且按平台隔离的渠道数: %+v", child)
|
||||
}
|
||||
call := func(method, path, body, actor string) map[string]any {
|
||||
t.Helper()
|
||||
req, _ := http.NewRequest(method, server.URL+path, strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Test-Actor", actor)
|
||||
response, err := server.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var value map[string]any
|
||||
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value["code"] != float64(0) {
|
||||
t.Fatalf("%s %s 失败: %+v", method, path, value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
created := call("POST", "/sw/business", `{"userName":"http_child","realName":"接口测试","password":"abc123"}`, "")["data"].(map[string]any)
|
||||
if created["userId"] != float64(4) || created["level"] != float64(1) || created["authorityId"] != float64(901) {
|
||||
t.Fatalf("HTTP 新增层级或角色不正确: %+v", created)
|
||||
}
|
||||
call("PUT", "/sw/business", `{"userId":4,"realName":"更新姓名"}`, "")
|
||||
updated := call("GET", "/sw/business/4", "", "")["data"].(map[string]any)
|
||||
if updated["realName"] != "更新姓名" || updated["status"] != "0" {
|
||||
t.Fatalf("HTTP 部分编辑不正确: %+v", updated)
|
||||
}
|
||||
call("PUT", "/sw/user/preference", `{"includeSubPromote":false,"idcardDedup":false}`, "")
|
||||
preference := call("GET", "/sw/user/preference", "", "")["data"].(map[string]any)
|
||||
if preference["includeSubPromote"] != false || preference["idcardDedup"] != false {
|
||||
t.Fatalf("HTTP 偏好关闭未保存: %+v", preference)
|
||||
}
|
||||
call("POST", "/sw/business/bindPlatform", `{"sourceDb":"lx","account":"sdk-test","password":"test-only"}`, "3")
|
||||
bound := call("GET", "/sw/business/bindPlatform", "", "3")["data"].(map[string]any)
|
||||
if bound["total"] != float64(1) || bound["rows"].([]any)[0].(map[string]any)["status"] != "3" {
|
||||
t.Fatalf("HTTP 绑定未进入待审核: %+v", bound)
|
||||
}
|
||||
call("DELETE", "/sw/business/bindPlatform", `{"sourceDb":"lx","platformBusierId":77}`, "3")
|
||||
bound = call("GET", "/sw/business/bindPlatform", "", "3")["data"].(map[string]any)
|
||||
if bound["total"] != float64(0) {
|
||||
t.Fatalf("HTTP 解绑未删除当前账号绑定: %+v", bound)
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+7
-1
@@ -24,6 +24,7 @@ import (
|
||||
integration4 "kra/internal/integration"
|
||||
"kra/internal/integration/cache"
|
||||
"kra/internal/integration/email"
|
||||
"kra/internal/integration/gameplatform"
|
||||
"kra/internal/integration/mq"
|
||||
"kra/internal/integration/payment"
|
||||
"kra/internal/integration/sms"
|
||||
@@ -87,7 +88,12 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg
|
||||
accountRepo := business.NewAccountRepo(dataData)
|
||||
accountUsecase := business2.NewAccountUsecase(accountRepo)
|
||||
accountService := business3.NewAccountService(accountUsecase)
|
||||
handlerBusiness := handler.NewBusiness(userPreferenceService, accountService)
|
||||
bindingRepo := business.NewBindingRepo(dataData)
|
||||
platformAccountRepo := business.NewPlatformAccountRepo(dataData)
|
||||
platformPasswordVerifier := gameplatform.NewPasswords()
|
||||
bindingUsecase := business2.NewBindingUsecase(bindingRepo, accountUsecase, platformAccountRepo, platformPasswordVerifier)
|
||||
bindingService := business3.NewBindingService(bindingUsecase)
|
||||
handlerBusiness := handler.NewBusiness(userPreferenceService, accountService, bindingService)
|
||||
runtimeconfigStore := data.NewIntegrationRuntime(dataData)
|
||||
smsSender := sms.NewSender(runtimeconfigStore)
|
||||
smsUsecase := integration.NewSMSUsecase(smsSender)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"base_url": "http://127.0.0.1:18000",
|
||||
"checks": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/sw/business/list?pageNum=1&pageSize=500",
|
||||
"http": 200,
|
||||
"code": 0,
|
||||
"msg": "成功",
|
||||
"elapsed_ms": 184,
|
||||
"passed": true,
|
||||
"total": 45,
|
||||
"rows": 45
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/sw/business/list?pageNum=2&pageSize=10",
|
||||
"http": 200,
|
||||
"code": 0,
|
||||
"msg": "成功",
|
||||
"elapsed_ms": 43,
|
||||
"passed": true,
|
||||
"total": 45,
|
||||
"rows": 10
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/sw/business/10001",
|
||||
"http": 200,
|
||||
"code": 0,
|
||||
"msg": "成功",
|
||||
"elapsed_ms": 19,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/sw/business/list?userName=admin",
|
||||
"http": 200,
|
||||
"code": 0,
|
||||
"msg": "成功",
|
||||
"elapsed_ms": 22,
|
||||
"passed": true,
|
||||
"total": 1,
|
||||
"rows": 1
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/sw/business/10007/bindPlatform",
|
||||
"http": 200,
|
||||
"code": 0,
|
||||
"msg": "成功",
|
||||
"elapsed_ms": 25,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/sw/user/preference",
|
||||
"http": 200,
|
||||
"code": 0,
|
||||
"msg": "成功",
|
||||
"elapsed_ms": 94,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/sw/business/bindPlatform?pageNum=1&pageSize=10",
|
||||
"http": 200,
|
||||
"code": 0,
|
||||
"msg": "成功",
|
||||
"elapsed_ms": 22,
|
||||
"passed": true,
|
||||
"total": 58,
|
||||
"rows": 10
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/sw/business/bindPlatform?sourceDb=lx,hw&status=0&pageSize=2",
|
||||
"http": 200,
|
||||
"code": 0,
|
||||
"msg": "成功",
|
||||
"elapsed_ms": 23,
|
||||
"passed": true,
|
||||
"total": 37,
|
||||
"rows": 2
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/sw/user/preference",
|
||||
"http": 200,
|
||||
"code": 7,
|
||||
"msg": "偏好参数不能为空",
|
||||
"elapsed_ms": 424,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/sw/business",
|
||||
"http": 200,
|
||||
"code": 7,
|
||||
"msg": "商务账号参数不正确",
|
||||
"elapsed_ms": 471,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/sw/business",
|
||||
"http": 200,
|
||||
"code": 7,
|
||||
"msg": "商务账号参数不正确",
|
||||
"elapsed_ms": 335,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/sw/business/bindPlatform",
|
||||
"http": 200,
|
||||
"code": 7,
|
||||
"msg": "商务账号参数不正确",
|
||||
"elapsed_ms": 521,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/sw/business/bindPlatform",
|
||||
"http": 200,
|
||||
"code": 7,
|
||||
"msg": "商务账号参数不正确",
|
||||
"elapsed_ms": 272,
|
||||
"passed": true
|
||||
}
|
||||
],
|
||||
"passed": true
|
||||
}
|
||||
@@ -45,10 +45,13 @@
|
||||
- 已落地商务用户七个扩展字段的存取及增量补列;登录和既有会话同时检查商务审核状态。
|
||||
- 已落地全局偏好 GET/PUT、当前用户隔离、缺省开启、显式关闭及幂等写入;Kra 顶栏增加统计口径入口。
|
||||
- 已落地商务列表、查询资料、新增子商务、部分编辑和审核状态,以及 Kra 风格 `BusinessList` 页面。层级及角色由服务端按原角色 ID 映射分配;账号、角色、偏好在同一事务创建。列表按平台与商务组合键统计直属一级渠道和已通过绑定。
|
||||
- 商务列表的绑定、画像、导出等功能仍在后续范围内;当前不将整个商务菜单标为完成。
|
||||
- 商务画像、旗下渠道明细、导出等功能仍在后续范围内;当前不将整个商务菜单标为完成。
|
||||
- 已接入绑定列表、指定商务已通过绑定详情、提交绑定及解绑四个接口,页面增加游戏 SDK 绑定页签与详情抽屉。绑定流程保持三级限制、平台验密、OA 待审核及驳回重提规则;新增键锁表 `sys_business_binding_locks`,串行处理同一平台商务的并发绑定。
|
||||
- 平台数据源通过 Kra 的 `data.database_list` 复用连接,别名为 `lx` / `hw` / `yq`。旧密码算法支持三种格式,兼容盐通过环境变量 `SW_SDK_AUTH_CODE`、`SW_SDK_MIGRATE_AUTH_CODE` 提供,取值应与旧 `XiguPasswordUtils` 对应常量一致;`SW_SDK_TABLE_PREFIX` 缺省为 `sys_`。真实连接凭据和盐不写进新仓库。完整平台库验密仍需部署配置后联调。
|
||||
- 已生成 `source-inventory.json`:现有 SQL 的 63 个接口契约、279 个变更源文件及额外找回密码控制器。此清单是范围索引,不代表已完成实现。
|
||||
- 已执行 `make generate`、相关 Go 测试、变更前端 ESLint 和生产构建。构建存在原 `arcdash` 依赖的 BigInt 浏览器目标警告。
|
||||
- 第一批提交 `ce4b54a` 后 `go test ./...` 全量通过;第二批账号功能的 biz/data/service/modules/server/cmd 检查及前端生产构建通过。
|
||||
- 用户反馈实际商务列表失败后,定位为 sw7 缺少 `tab_promote`。已解耦基础列表与渠道统计:有绑定且渠道表缺失时返回 `promoteCount: null`,界面显示“未接入”,无绑定账号仍为真实零。新增通过真实 TCP 请求的 HTTP 回归测试,覆盖系统数据库及完整渠道表两种场景。
|
||||
- 已通过正常验证码登录取得测试会话,并在修复后的独立 18000 实例连接同一 sw7 执行 HTTP 请求。列表总数 45,分页、账号筛选、资料和偏好查询全部成功。详见 `http-verification-2026-11-03.json`。8000 为用户 GoLand 旧二进制,需重新运行才加载源码修复。
|
||||
- 绑定批次通过 13 项真实 HTTP 检查:商务 45 条、绑定 58 条、平台与审核筛选、详情、偏好查询和写请求校验,结果见 `http-business-bindings-2026-11-03.json`;`scripts/sw_http_smoke.py` 可复用且不记录令牌。真实库不制造测试商务或发起实际平台绑定。新增、修改、偏好关闭、提交绑定及解绑的正向流程已用独立 SQLite 数据库通过实际 TCP HTTP 测试;平台验密外部边界使用测试替身,密码算法另用固定向量验证。
|
||||
- 当前没有执行本轮真实数据库迁移验证。
|
||||
|
||||
+1
-1
@@ -11,4 +11,4 @@ import (
|
||||
)
|
||||
|
||||
// ProviderSet is biz providers.
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase)
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPlatform = errors.BadRequest("SW_PLATFORM_INVALID", "所属平台不正确")
|
||||
ErrPlatformUnavailable = errors.BadRequest("SW_PLATFORM_UNAVAILABLE", "平台数据源未配置或暂不可用")
|
||||
ErrPlatformPasswordConfig = errors.BadRequest("SW_PLATFORM_PASSWORD_CONFIG", "平台密码兼容配置未就绪")
|
||||
ErrPlatformAccount = errors.BadRequest("SW_PLATFORM_ACCOUNT", "游戏商务账号不存在或已禁用")
|
||||
ErrPlatformPassword = errors.BadRequest("SW_PLATFORM_PASSWORD", "游戏商务账号或密码错误")
|
||||
ErrBindingLevel = errors.BadRequest("SW_BINDING_LEVEL", "仅三级商务可绑定游戏 SDK 商务")
|
||||
ErrBindingOccupied = errors.BadRequest("SW_BINDING_OCCUPIED", "该游戏商务已被其他账号绑定或审核中")
|
||||
ErrBindingExists = errors.BadRequest("SW_BINDING_EXISTS", "该游戏商务已绑定")
|
||||
ErrBindingPending = errors.BadRequest("SW_BINDING_PENDING", "该游戏商务已提交,请等待 OA 审核")
|
||||
ErrBindingNotFound = errors.NotFound("SW_BINDING_NOT_FOUND", "绑定记录不存在")
|
||||
)
|
||||
|
||||
type PlatformKey struct {
|
||||
SourceDB string
|
||||
BusierID uint64
|
||||
}
|
||||
type PlatformAccount struct {
|
||||
ID uint64
|
||||
Account, Password string
|
||||
Status int
|
||||
}
|
||||
type PlatformAccountRepo interface {
|
||||
FindByAccount(context.Context, string, string) (*PlatformAccount, error)
|
||||
}
|
||||
type PlatformPasswordVerifier interface {
|
||||
Verify(string, string) (bool, error)
|
||||
}
|
||||
|
||||
func NormalizePlatform(value string) (string, error) {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
switch value {
|
||||
case "lx", "hw", "yq":
|
||||
return value, nil
|
||||
default:
|
||||
return "", ErrPlatform
|
||||
}
|
||||
}
|
||||
func PlatformName(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "lx":
|
||||
return "龙翔"
|
||||
case "hw":
|
||||
return "汇玩"
|
||||
case "yq":
|
||||
return "游趣"
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
type Binding struct {
|
||||
UserID uint
|
||||
PlatformKey
|
||||
Account, UserName string
|
||||
Status string
|
||||
RejectReason *string
|
||||
CreatedAt time.Time
|
||||
ReviewTime *time.Time
|
||||
ReviewAccount *string
|
||||
PromoteCount *int64
|
||||
}
|
||||
|
||||
type BindingFilter struct {
|
||||
UserID uint
|
||||
SourceDBs []string
|
||||
Account string
|
||||
Status string
|
||||
}
|
||||
type BindingRepo interface {
|
||||
List(context.Context, UserScope, BindingFilter, ...ListOption) ([]*Binding, int64, error)
|
||||
WithPlatformLock(context.Context, PlatformKey, func(context.Context) error) error
|
||||
ForPlatform(context.Context, PlatformKey) ([]*Binding, error)
|
||||
Save(context.Context, *Binding) error
|
||||
Delete(context.Context, uint, PlatformKey) error
|
||||
}
|
||||
|
||||
type BindingUsecase struct {
|
||||
repo BindingRepo
|
||||
accounts *AccountUsecase
|
||||
platforms PlatformAccountRepo
|
||||
passwords PlatformPasswordVerifier
|
||||
}
|
||||
|
||||
func NewBindingUsecase(repo BindingRepo, accounts *AccountUsecase, platforms PlatformAccountRepo, passwords PlatformPasswordVerifier) *BindingUsecase {
|
||||
return &BindingUsecase{repo: repo, accounts: accounts, platforms: platforms, passwords: passwords}
|
||||
}
|
||||
|
||||
func (uc *BindingUsecase) List(ctx context.Context, q BindingFilter, options ...ListOption) ([]*Binding, int64, error) {
|
||||
actor, current, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
scope := UserScope{All: actor.AuthorityID == 888}
|
||||
if !scope.All {
|
||||
scope.SelfID = current.ID
|
||||
if current.Level >= 3 {
|
||||
scope.OnlySelf = true
|
||||
} else {
|
||||
scope.DescendantPath = descendantPath(current)
|
||||
}
|
||||
}
|
||||
return uc.repo.List(ctx, scope, q, options...)
|
||||
}
|
||||
|
||||
func (uc *BindingUsecase) ForUser(ctx context.Context, id uint) ([]*Binding, error) {
|
||||
if _, err := uc.accounts.Find(ctx, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, _, err := uc.repo.List(ctx, UserScope{SelfID: id, OnlySelf: true}, BindingFilter{UserID: id, Status: "0"}, ListLimit(-1))
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (uc *BindingUsecase) Bind(ctx context.Context, source, account, password string) error {
|
||||
_, current, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current.Level != 3 {
|
||||
return ErrBindingLevel
|
||||
}
|
||||
source, err = NormalizePlatform(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account = strings.TrimSpace(account)
|
||||
platform, err := uc.platforms.FindByAccount(ctx, source, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if platform == nil || platform.Status != 1 {
|
||||
return ErrPlatformAccount
|
||||
}
|
||||
valid, err := uc.passwords.Verify(password, platform.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return ErrPlatformPassword
|
||||
}
|
||||
key := PlatformKey{SourceDB: source, BusierID: platform.ID}
|
||||
// 平台验密完成后才开启主库事务,避免跨库事务切源。
|
||||
return uc.repo.WithPlatformLock(ctx, key, func(txctx context.Context) error {
|
||||
bindings, err := uc.repo.ForPlatform(txctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, b := range bindings {
|
||||
if b.UserID != current.ID && (b.Status == "0" || b.Status == "3") {
|
||||
return ErrBindingOccupied
|
||||
}
|
||||
}
|
||||
for _, b := range bindings {
|
||||
if b.UserID == current.ID {
|
||||
if b.Status == "0" {
|
||||
return ErrBindingExists
|
||||
}
|
||||
if b.Status == "3" {
|
||||
return ErrBindingPending
|
||||
}
|
||||
}
|
||||
}
|
||||
return uc.repo.Save(txctx, &Binding{UserID: current.ID, PlatformKey: key, Account: account, Status: "3", CreatedAt: time.Now()})
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *BindingUsecase) Unbind(ctx context.Context, key PlatformKey) error {
|
||||
_, current, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.SourceDB, err = NormalizePlatform(key.SourceDB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key.BusierID == 0 {
|
||||
return ErrBindingNotFound
|
||||
}
|
||||
return uc.repo.WithPlatformLock(ctx, key, func(txctx context.Context) error { return uc.repo.Delete(txctx, current.ID, key) })
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type bindingRepoFake struct {
|
||||
BindingRepo
|
||||
mu sync.Mutex
|
||||
rows []*Binding
|
||||
saved *Binding
|
||||
scope UserScope
|
||||
filter BindingFilter
|
||||
}
|
||||
|
||||
func (r *bindingRepoFake) WithPlatformLock(ctx context.Context, _ PlatformKey, fn func(context.Context) error) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return fn(ctx)
|
||||
}
|
||||
func (r *bindingRepoFake) ForPlatform(context.Context, PlatformKey) ([]*Binding, error) {
|
||||
return r.rows, nil
|
||||
}
|
||||
func (r *bindingRepoFake) Save(_ context.Context, b *Binding) error {
|
||||
r.saved = b
|
||||
r.rows = []*Binding{b}
|
||||
return nil
|
||||
}
|
||||
func (r *bindingRepoFake) List(_ context.Context, s UserScope, q BindingFilter, _ ...ListOption) ([]*Binding, int64, error) {
|
||||
r.scope, r.filter = s, q
|
||||
return []*Binding{}, 0, nil
|
||||
}
|
||||
|
||||
type platformRepoFake struct {
|
||||
account *PlatformAccount
|
||||
calls int
|
||||
}
|
||||
|
||||
func (r *platformRepoFake) FindByAccount(context.Context, string, string) (*PlatformAccount, error) {
|
||||
r.calls++
|
||||
return r.account, nil
|
||||
}
|
||||
|
||||
type passwordFake struct{}
|
||||
|
||||
func (passwordFake) Verify(string, string) (bool, error) { return true, nil }
|
||||
|
||||
func newBindingTestUsecase(repo *bindingRepoFake) (*BindingUsecase, *platformRepoFake) {
|
||||
a := &accountRepoFake{accounts: map[uint]*Account{1: {ID: 1, Level: 1, IDPath: "0"}, 2: {ID: 2, Level: 2, IDPath: "0,1", ParentID: 1}, 3: {ID: 3, Level: 3, IDPath: "0,1,2", ParentID: 2}, 4: {ID: 4, Level: 3, IDPath: "0,1,2", ParentID: 2}}}
|
||||
p := &platformRepoFake{account: &PlatformAccount{ID: 7, Status: 1}}
|
||||
return NewBindingUsecase(repo, NewAccountUsecase(a), p, passwordFake{}), p
|
||||
}
|
||||
|
||||
func TestBindingTransitionsAndLevel(t *testing.T) {
|
||||
for _, item := range []struct {
|
||||
name string
|
||||
existing *Binding
|
||||
want error
|
||||
}{
|
||||
{"新增", nil, nil}, {"重提驳回", &Binding{UserID: 3, Status: "2"}, nil}, {"本人通过", &Binding{UserID: 3, Status: "0"}, ErrBindingExists},
|
||||
{"本人待审核", &Binding{UserID: 3, Status: "3"}, ErrBindingPending}, {"其他账号通过", &Binding{UserID: 4, Status: "0"}, ErrBindingOccupied}, {"其他账号待审核", &Binding{UserID: 4, Status: "3"}, ErrBindingOccupied},
|
||||
} {
|
||||
t.Run(item.name, func(t *testing.T) {
|
||||
r := &bindingRepoFake{}
|
||||
if item.existing != nil {
|
||||
r.rows = []*Binding{item.existing}
|
||||
}
|
||||
uc, _ := newBindingTestUsecase(r)
|
||||
if err := uc.Bind(accountContext(3, 903), "LX", " sdk ", "password"); err != item.want {
|
||||
t.Fatalf("绑定结果错误: %v", err)
|
||||
}
|
||||
if item.want == nil && (r.saved.Status != "3" || r.saved.Account != "sdk" || r.saved.SourceDB != "lx" || r.saved.RejectReason != nil || r.saved.ReviewTime != nil || r.saved.ReviewAccount != nil) {
|
||||
t.Fatalf("待审核状态未正确重置: %+v", r.saved)
|
||||
}
|
||||
})
|
||||
}
|
||||
r := &bindingRepoFake{}
|
||||
uc, p := newBindingTestUsecase(r)
|
||||
if err := uc.Bind(accountContext(2, 902), "lx", "sdk", "password"); err != ErrBindingLevel || p.calls != 0 {
|
||||
t.Fatal("非三级不应访问平台验密")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingScopesMatchBusinessLevels(t *testing.T) {
|
||||
r := &bindingRepoFake{}
|
||||
uc, _ := newBindingTestUsecase(r)
|
||||
if _, _, err := uc.List(accountContext(1, 901), BindingFilter{}); err != nil || r.scope.IncludeSelf || r.scope.OnlySelf || r.scope.DescendantPath != "0,1" {
|
||||
t.Fatalf("一级仅查看下级绑定: %+v %v", r.scope, err)
|
||||
}
|
||||
if _, _, err := uc.List(accountContext(3, 903), BindingFilter{}); err != nil || !r.scope.OnlySelf || r.scope.SelfID != 3 {
|
||||
t.Fatal("三级只能查看自己的绑定")
|
||||
}
|
||||
if _, err := uc.ForUser(accountContext(1, 901), 3); err != nil || r.filter.Status != "0" || r.filter.UserID != 3 {
|
||||
t.Fatal("绑定详情必须仅已通过")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingConcurrentSubmissionHasOneOwner(t *testing.T) {
|
||||
r := &bindingRepoFake{}
|
||||
uc, _ := newBindingTestUsecase(r)
|
||||
// 每个用例独立的平台桩避免把桩的计数器带进并发断言。
|
||||
other, _ := newBindingTestUsecase(r)
|
||||
errors := make(chan error, 2)
|
||||
go func() { errors <- uc.Bind(accountContext(3, 903), "lx", "sdk", "password") }()
|
||||
go func() { errors <- other.Bind(accountContext(4, 903), "lx", "sdk", "password") }()
|
||||
first, second := <-errors, <-errors
|
||||
if !((first == nil && second == ErrBindingOccupied) || (second == nil && first == ErrBindingOccupied)) {
|
||||
t.Fatalf("同一平台商务必须独占: %v %v", first, second)
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ func checkAccountUnique(tx *gorm.DB, a *business.Account) error {
|
||||
}
|
||||
|
||||
func (r *accountRepo) Create(ctx context.Context, a *business.Account) error {
|
||||
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := checkAccountUnique(tx, a); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,10 +171,11 @@ func (r *accountRepo) Create(ctx context.Context, a *business.Account) error {
|
||||
a.ID, a.CreatedAt, a.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
|
||||
return nil
|
||||
})
|
||||
return storageError(err)
|
||||
}
|
||||
|
||||
func (r *accountRepo) Update(ctx context.Context, a *business.Account) error {
|
||||
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := checkAccountUnique(tx, a); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -194,6 +195,7 @@ func (r *accountRepo) Update(ctx context.Context, a *business.Account) error {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return storageError(err)
|
||||
}
|
||||
|
||||
func (r *accountRepo) AuthorityForLevel(ctx context.Context, level int) (uint, error) {
|
||||
@@ -250,17 +252,10 @@ func (r *accountRepo) DirectCounts(ctx context.Context, ids []uint) (map[uint]bu
|
||||
result[b.UserID] = business.AccountCounts{PromoteCount: &zero, BindCount: b.Total}
|
||||
}
|
||||
// 系统数据演练库尚未包含渠道表时,基础资料与已通过绑定数仍可使用。
|
||||
tables, err := db.Migrator().GetTables()
|
||||
available, err := businessTableAvailable(db, "tab_promote")
|
||||
if err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
available := false
|
||||
for _, table := range tables {
|
||||
if table == "tab_promote" {
|
||||
available = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !available {
|
||||
for _, b := range binds {
|
||||
value := result[b.UserID]
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
type bindingLockPO struct {
|
||||
SourceDB string `gorm:"column:source_db;primaryKey;size:50"`
|
||||
PlatformBusierID uint64 `gorm:"primaryKey;autoIncrement:false"`
|
||||
}
|
||||
|
||||
func (bindingLockPO) TableName() string { return "sys_business_binding_locks" }
|
||||
|
||||
type bindingRepo struct{ data Provider }
|
||||
|
||||
func NewBindingRepo(data Provider) business.BindingRepo { return &bindingRepo{data: data} }
|
||||
|
||||
type bindingTxKey struct{}
|
||||
|
||||
func (r *bindingRepo) db(ctx context.Context) *gorm.DB {
|
||||
if tx, ok := ctx.Value(bindingTxKey{}).(*gorm.DB); ok {
|
||||
return tx.WithContext(ctx)
|
||||
}
|
||||
return r.data.DB().WithContext(ctx)
|
||||
}
|
||||
func (r *bindingRepo) WithPlatformLock(ctx context.Context, key business.PlatformKey, fn func(context.Context) error) error {
|
||||
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 单独的键锁覆盖“尚无绑定记录”的并发场景,不修改平台数据库。
|
||||
lock := bindingLockPO{SourceDB: key.SourceDB, PlatformBusierID: key.BusierID}
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&lock).Error; err != nil {
|
||||
return business.ErrStorage
|
||||
}
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("source_db = ? AND platform_busier_id = ?", key.SourceDB, key.BusierID).First(&lock).Error; err != nil {
|
||||
return business.ErrStorage
|
||||
}
|
||||
return fn(context.WithValue(ctx, bindingTxKey{}, tx))
|
||||
})
|
||||
return storageError(err)
|
||||
}
|
||||
|
||||
func toBizBinding(po bindingPO) *business.Binding {
|
||||
return &business.Binding{UserID: po.UserID, PlatformKey: business.PlatformKey{SourceDB: po.SourceDB, BusierID: po.PlatformBusierID}, Account: po.PlatformBusierAccount,
|
||||
Status: po.Status, RejectReason: po.RejectReason, CreatedAt: po.CreateTime, ReviewTime: po.ReviewTime, ReviewAccount: po.ReviewAccount}
|
||||
}
|
||||
func newBinding(b *business.Binding) bindingPO {
|
||||
return bindingPO{UserID: b.UserID, SourceDB: b.SourceDB, PlatformBusierID: b.BusierID, PlatformBusierAccount: b.Account, Status: b.Status,
|
||||
RejectReason: b.RejectReason, CreateTime: b.CreatedAt, ReviewTime: b.ReviewTime, ReviewAccount: b.ReviewAccount}
|
||||
}
|
||||
func (r *bindingRepo) ForPlatform(ctx context.Context, key business.PlatformKey) ([]*business.Binding, error) {
|
||||
var pos []bindingPO
|
||||
if err := r.db(ctx).Where("source_db = ? AND platform_busier_id = ?", key.SourceDB, key.BusierID).Find(&pos).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
rows := make([]*business.Binding, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
rows = append(rows, toBizBinding(po))
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
func (r *bindingRepo) Save(ctx context.Context, b *business.Binding) error {
|
||||
po := newBinding(b)
|
||||
err := r.db(ctx).Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "user_id"}, {Name: "source_db"}, {Name: "platform_busier_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"platform_busier_account", "status", "reject_reason", "create_time", "review_time", "review_account"})}).Create(&po).Error
|
||||
if err != nil {
|
||||
return business.ErrStorage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (r *bindingRepo) Delete(ctx context.Context, userID uint, key business.PlatformKey) error {
|
||||
result := r.db(ctx).Where("user_id = ? AND source_db = ? AND platform_busier_id = ?", userID, key.SourceDB, key.BusierID).Delete(&bindingPO{})
|
||||
if result.Error != nil {
|
||||
return business.ErrStorage
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return business.ErrBindingNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *bindingRepo) List(ctx context.Context, scope business.UserScope, q business.BindingFilter, options ...business.ListOption) ([]*business.Binding, int64, error) {
|
||||
db := r.db(ctx).Table("sys_user_bind_platform b").Joins("JOIN sys_users u ON u.id=b.user_id AND u.deleted_at IS NULL")
|
||||
db = applyUserScope(db, scope, "u.")
|
||||
if q.UserID != 0 {
|
||||
db = db.Where("b.user_id = ?", q.UserID)
|
||||
}
|
||||
if len(q.SourceDBs) > 0 {
|
||||
db = db.Where("b.source_db IN ?", q.SourceDBs)
|
||||
}
|
||||
if q.Account != "" {
|
||||
db = db.Where("b.platform_busier_account LIKE ?", "%"+q.Account+"%")
|
||||
}
|
||||
if q.Status != "" {
|
||||
db = db.Where("b.status = ?", q.Status)
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, business.ErrStorage
|
||||
}
|
||||
o := business.NewListOptions(options...)
|
||||
var pos []struct {
|
||||
Binding bindingPO `gorm:"embedded"`
|
||||
UserName string
|
||||
}
|
||||
if err := db.Select("b.*,u.username AS user_name").Order("b.create_time DESC").Order("b.source_db,b.platform_busier_id,b.user_id").Offset(o.Offset).Limit(o.Limit).Scan(&pos).Error; err != nil {
|
||||
return nil, 0, business.ErrStorage
|
||||
}
|
||||
rows := make([]*business.Binding, 0, len(pos))
|
||||
keys := make(map[business.PlatformKey]bool)
|
||||
for _, po := range pos {
|
||||
b := toBizBinding(po.Binding)
|
||||
zero := int64(0)
|
||||
b.PromoteCount = &zero
|
||||
b.UserName = po.UserName
|
||||
rows = append(rows, b)
|
||||
if b.Status == "0" {
|
||||
keys[b.PlatformKey] = true
|
||||
}
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return rows, total, nil
|
||||
}
|
||||
available, err := businessTableAvailable(r.db(ctx), "tab_promote")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if !available {
|
||||
for _, b := range rows {
|
||||
if b.Status == "0" {
|
||||
b.PromoteCount = nil
|
||||
}
|
||||
}
|
||||
return rows, total, nil
|
||||
}
|
||||
query := r.db(ctx).Table("tab_promote").Where("promote_level = ?", 1)
|
||||
pairs := r.db(ctx).Session(&gorm.Session{NewDB: true})
|
||||
for key := range keys {
|
||||
pairs = pairs.Or("(source_db = ? AND busier_id = ?)", key.SourceDB, key.BusierID)
|
||||
}
|
||||
var counts []struct {
|
||||
SourceDB string `gorm:"column:source_db"`
|
||||
BusierID uint64
|
||||
Total int64
|
||||
}
|
||||
if err := query.Where(pairs).Select("source_db,busier_id,COUNT(*) AS total").Group("source_db,busier_id").Scan(&counts).Error; err != nil {
|
||||
return nil, 0, business.ErrStorage
|
||||
}
|
||||
byKey := make(map[business.PlatformKey]int64, len(counts))
|
||||
for _, c := range counts {
|
||||
byKey[business.PlatformKey{SourceDB: c.SourceDB, BusierID: c.BusierID}] = c.Total
|
||||
}
|
||||
for _, b := range rows {
|
||||
if b.Status == "0" {
|
||||
count := byKey[b.PlatformKey]
|
||||
b.PromoteCount = &count
|
||||
}
|
||||
}
|
||||
return rows, total, nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBindingRepositoryClearsAuditAndUsesCompositeIdentity(t *testing.T) {
|
||||
db := prepareAccounts(t)
|
||||
r := NewBindingRepo(testData{db})
|
||||
ctx := context.Background()
|
||||
reason, review := "旧驳回", "审核员"
|
||||
now := time.Now()
|
||||
key := business.PlatformKey{SourceDB: "lx", BusierID: 7}
|
||||
if err := r.Save(ctx, &business.Binding{UserID: 3, PlatformKey: key, Status: "2", RejectReason: &reason, ReviewTime: &now, ReviewAccount: &review}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.Save(ctx, &business.Binding{UserID: 3, PlatformKey: business.PlatformKey{SourceDB: "hw", BusierID: 7}, Status: "0"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.WithPlatformLock(ctx, key, func(txctx context.Context) error {
|
||||
return r.Save(txctx, &business.Binding{UserID: 3, PlatformKey: key, Status: "3", CreatedAt: now})
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, err := r.ForPlatform(ctx, key)
|
||||
if err != nil || len(rows) != 1 {
|
||||
t.Fatalf("查询失败: %+v %v", rows, err)
|
||||
}
|
||||
if rows[0].Status != "3" || rows[0].RejectReason != nil || rows[0].ReviewTime != nil || rows[0].ReviewAccount != nil {
|
||||
t.Fatal("重提未清除旧审核信息")
|
||||
}
|
||||
if err := r.Delete(ctx, 4, key); err != business.ErrBindingNotFound {
|
||||
t.Fatal("不能删除其他用户绑定")
|
||||
}
|
||||
if err := r.Delete(ctx, 3, key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other, err := r.ForPlatform(ctx, business.PlatformKey{SourceDB: "hw", BusierID: 7})
|
||||
if err != nil || len(other) != 1 {
|
||||
t.Fatal("解绑影响了同 ID 的其他平台")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingListLoadsDTOFieldsAndSurvivesMissingChannels(t *testing.T) {
|
||||
db := prepareAccounts(t)
|
||||
if err := db.Create(&accountPO{ID: 3, Username: "child", Level: 3, IDPath: "0,1,2"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := NewBindingRepo(testData{db})
|
||||
ctx := context.Background()
|
||||
for _, source := range []string{"lx", "hw"} {
|
||||
if err := r.Save(ctx, &business.Binding{UserID: 3, PlatformKey: business.PlatformKey{SourceDB: source, BusierID: 7}, Account: "sdk", Status: "0", CreatedAt: time.Now()}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := db.Exec("INSERT INTO tab_promote VALUES(1,'lx',7,1,1),(2,'lx',7,2,1)").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, total, err := r.List(ctx, business.UserScope{OnlySelf: true, SelfID: 3}, business.BindingFilter{SourceDBs: []string{"lx"}})
|
||||
if err != nil || total != 1 || len(rows) != 1 {
|
||||
t.Fatalf("绑定列表错误: %+v %d %v", rows, total, err)
|
||||
}
|
||||
if rows[0].UserID != 3 || rows[0].UserName != "child" || rows[0].Account != "sdk" || rows[0].PromoteCount == nil || *rows[0].PromoteCount != 1 {
|
||||
t.Fatalf("绑定字段或渠道统计错误: %+v", rows[0])
|
||||
}
|
||||
if err := db.Exec("DROP TABLE tab_promote").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, total, err = r.List(ctx, business.UserScope{OnlySelf: true, SelfID: 3}, business.BindingFilter{})
|
||||
if err != nil || total != 2 || len(rows) != 2 || rows[0].PromoteCount != nil {
|
||||
t.Fatalf("缺渠道表不应阻断绑定资料: %+v %d %v", rows, total, err)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
)
|
||||
|
||||
func Migrations() []migration.Step {
|
||||
return []migration.Step{{ID: "202611030003_business_bindings", Migrate: func(db *gorm.DB) error {
|
||||
return []migration.Step{{ID: "202611030004_business_binding_locks", Migrate: func(db *gorm.DB) error {
|
||||
return migration.CreateMissingTables(db, &bindingLockPO{})
|
||||
}}, {ID: "202611030003_business_bindings", Migrate: func(db *gorm.DB) error {
|
||||
return migration.CreateMissingTables(db, &bindingPO{})
|
||||
}}, {ID: "202611030001_business_preference", Migrate: func(db *gorm.DB) error {
|
||||
// 旧表沿用;已迁移数据库不重建或重置用户偏好。
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
type PlatformProvider interface {
|
||||
Database(string) (*gorm.DB, error)
|
||||
}
|
||||
type platformAccountRepo struct{ data PlatformProvider }
|
||||
|
||||
func NewPlatformAccountRepo(data PlatformProvider) business.PlatformAccountRepo {
|
||||
return &platformAccountRepo{data: data}
|
||||
}
|
||||
|
||||
type platformAccountPO struct {
|
||||
ID uint64
|
||||
Account, Password string
|
||||
Status int
|
||||
}
|
||||
|
||||
func (platformAccountPO) TableName() string { return "tab_promote_business" }
|
||||
|
||||
func (r *platformAccountRepo) FindByAccount(ctx context.Context, source, account string) (*business.PlatformAccount, error) {
|
||||
source, err := business.NormalizePlatform(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := r.data.Database(source)
|
||||
if err != nil || db == nil {
|
||||
return nil, business.ErrPlatformUnavailable
|
||||
}
|
||||
var po platformAccountPO
|
||||
err = db.WithContext(ctx).Where("account = ?", account).First(&po).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, business.ErrPlatformUnavailable
|
||||
}
|
||||
return &business.PlatformAccount{ID: po.ID, Account: po.Account, Password: po.Password, Status: po.Status}, nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"errors"
|
||||
kratoserrors "github.com/go-kratos/kratos/v3/errors"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
func storageError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var typed *kratoserrors.Error
|
||||
if errors.As(err, &typed) {
|
||||
return err
|
||||
}
|
||||
return business.ErrStorage
|
||||
}
|
||||
|
||||
func businessTableAvailable(db *gorm.DB, name string) (bool, error) {
|
||||
tables, err := db.Migrator().GetTables()
|
||||
if err != nil {
|
||||
return false, business.ErrStorage
|
||||
}
|
||||
for _, table := range tables {
|
||||
if table == name {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -27,8 +27,11 @@ import (
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewData,
|
||||
wire.Bind(new(databusiness.Provider), new(*Data)),
|
||||
wire.Bind(new(databusiness.PlatformProvider), new(*Data)),
|
||||
databusiness.NewUserPreferenceRepo,
|
||||
databusiness.NewAccountRepo,
|
||||
databusiness.NewBindingRepo,
|
||||
databusiness.NewPlatformAccountRepo,
|
||||
NewIntegrationRuntime,
|
||||
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||
|
||||
@@ -34,8 +34,8 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||
if err = db.Table(migration.TableName).Count(&versions).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if versions != 17 {
|
||||
t.Fatalf("migration versions = %d, want 17", versions)
|
||||
if versions != 18 {
|
||||
t.Fatalf("migration versions = %d, want 18", versions)
|
||||
}
|
||||
var communicationRows []dataintegration.ConfigPO
|
||||
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package gameplatform
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"kra/internal/biz/business"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Passwords struct{ AuthCode, MigrateAuthCode, TablePrefix string }
|
||||
|
||||
func NewPasswords() business.PlatformPasswordVerifier {
|
||||
prefix := os.Getenv("SW_SDK_TABLE_PREFIX")
|
||||
if prefix == "" {
|
||||
prefix = "sys_"
|
||||
}
|
||||
return &Passwords{AuthCode: os.Getenv("SW_SDK_AUTH_CODE"), MigrateAuthCode: os.Getenv("SW_SDK_MIGRATE_AUTH_CODE"), TablePrefix: prefix}
|
||||
}
|
||||
func md5Hex(value string) string { sum := md5.Sum([]byte(value)); return hex.EncodeToString(sum[:]) }
|
||||
|
||||
func (p *Passwords) Verify(password, stored string) (bool, error) {
|
||||
if strings.TrimSpace(password) == "" || strings.TrimSpace(stored) == "" {
|
||||
return false, nil
|
||||
}
|
||||
var expected string
|
||||
switch {
|
||||
case strings.HasPrefix(stored, "###"):
|
||||
if p.AuthCode == "" {
|
||||
return false, business.ErrPlatformPasswordConfig
|
||||
}
|
||||
expected = "###" + md5Hex(md5Hex(p.AuthCode+password))
|
||||
case strings.HasPrefix(stored, "##$##"):
|
||||
if p.MigrateAuthCode == "" {
|
||||
return false, business.ErrPlatformPasswordConfig
|
||||
}
|
||||
sha := sha1.Sum([]byte(password))
|
||||
expected = "##$##" + md5Hex(hex.EncodeToString(sha[:])+p.MigrateAuthCode)
|
||||
default:
|
||||
decor := md5Hex(p.TablePrefix)
|
||||
expected = decor[:12] + md5Hex(password) + decor[len(decor)-4:]
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(expected), []byte(stored)) == 1, nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package gameplatform
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLegacyPasswordFormats(t *testing.T) {
|
||||
p := &Passwords{AuthCode: "test-auth", MigrateAuthCode: "test-migrate", TablePrefix: "sys_"}
|
||||
// 固定向量由独立的 Python hashlib 按原 Java 算法生成。
|
||||
for _, hash := range []string{"###eef6d9b7160f32a869f3e03b7ea575a5", "9db2515d8fcc62cc2d8b4bf2d8728120d052163a77df7f09", "##$##073cf9a979400c63c0dc69f201efcdf8"} {
|
||||
if ok, err := p.Verify("demo123", hash); err != nil || !ok {
|
||||
t.Fatalf("旧密码格式不兼容: %v", err)
|
||||
}
|
||||
if ok, _ := p.Verify("wrong", hash); ok {
|
||||
t.Fatal("错误密码被接受")
|
||||
}
|
||||
}
|
||||
if ok, err := (&Passwords{}).Verify("demo123", "###eef6d9b7160f32a869f3e03b7ea575a5"); ok || err == nil {
|
||||
t.Fatal("缺少兼容盐时必须明确报错")
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"kra/internal/biz/system"
|
||||
"kra/internal/integration/cache"
|
||||
"kra/internal/integration/email"
|
||||
"kra/internal/integration/gameplatform"
|
||||
mqintegration "kra/internal/integration/mq"
|
||||
paymentintegration "kra/internal/integration/payment"
|
||||
"kra/internal/integration/sms"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
)
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
gameplatform.NewPasswords,
|
||||
cache.New,
|
||||
email.NewEmailRepo,
|
||||
sms.NewSender,
|
||||
|
||||
@@ -25,16 +25,16 @@ func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) {
|
||||
t.Fatalf("definition[%d] = %q, want %q", index, got, name)
|
||||
}
|
||||
}
|
||||
if got := catalog.MigrationSteps(); len(got) != 17 {
|
||||
t.Fatalf("module migrations = %d, want 17", len(got))
|
||||
if got := catalog.MigrationSteps(); len(got) != 18 {
|
||||
t.Fatalf("module migrations = %d, want 18", len(got))
|
||||
}
|
||||
var menus, apis int
|
||||
for _, definition := range catalog.Definitions {
|
||||
menus += len(definition.Surface.Menus)
|
||||
apis += len(definition.Surface.APIs)
|
||||
}
|
||||
if menus != 6 || apis != 24 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 6/24", menus, apis)
|
||||
if menus != 6 || apis != 28 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 6/28", menus, apis)
|
||||
}
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
||||
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
||||
|
||||
@@ -41,6 +41,10 @@ type routeValue struct {
|
||||
// Gin router files still bind handlers; tests keep those registrations aligned
|
||||
// with this catalog while public, audit and API metadata live only here.
|
||||
var routes = map[string]routeValue{
|
||||
"GET /sw/business/bindPlatform": {group: "商务管理", description: "查询游戏商务绑定"},
|
||||
"GET /sw/business/:userId/bindPlatform": {group: "商务管理", description: "查询指定商务已通过的绑定"},
|
||||
"POST /sw/business/bindPlatform": {group: "商务管理", description: "绑定游戏商务并提交审核", audit: true},
|
||||
"DELETE /sw/business/bindPlatform": {group: "商务管理", description: "解绑当前账号的游戏商务", audit: true},
|
||||
"GET /sw/business/list": {group: "商务管理", description: "查询下级商务列表"},
|
||||
"GET /sw/business/:userId": {group: "商务管理", description: "获取商务资料"},
|
||||
"POST /sw/business": {group: "商务管理", description: "添加子商务", audit: true},
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
||||
t.Fatalf("registered route log count = %d, want %d", got, want)
|
||||
}
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":203`) {
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":207`) {
|
||||
t.Fatalf("startup route summary is missing: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,62 @@ import (
|
||||
type Business struct {
|
||||
preference *business.UserPreferenceService
|
||||
accounts *business.AccountService
|
||||
bindings *business.BindingService
|
||||
}
|
||||
|
||||
func NewBusiness(preference *business.UserPreferenceService, accounts *business.AccountService) *Business {
|
||||
return &Business{preference: preference, accounts: accounts}
|
||||
func NewBusiness(preference *business.UserPreferenceService, accounts *business.AccountService, bindings *business.BindingService) *Business {
|
||||
return &Business{preference: preference, accounts: accounts, bindings: bindings}
|
||||
}
|
||||
|
||||
func (h *Business) BindingList(c *gin.Context) {
|
||||
var req dto.BusinessBindingQuery
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
Fail(c, "绑定查询参数格式错误")
|
||||
return
|
||||
}
|
||||
rows, err := h.bindings.List(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, rows)
|
||||
}
|
||||
func (h *Business) UserBindings(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("userId"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
Fail(c, "商务用户 ID 不正确")
|
||||
return
|
||||
}
|
||||
rows, err := h.bindings.ForUser(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, rows)
|
||||
}
|
||||
func (h *Business) BindPlatform(c *gin.Context) {
|
||||
var req dto.BusinessBindingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Fail(c, "绑定参数格式错误")
|
||||
return
|
||||
}
|
||||
if err := h.bindings.Bind(c.Request.Context(), &req); err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OK(c)
|
||||
}
|
||||
func (h *Business) UnbindPlatform(c *gin.Context) {
|
||||
var req dto.BusinessBindingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Fail(c, "解绑参数格式错误")
|
||||
return
|
||||
}
|
||||
if err := h.bindings.Unbind(c.Request.Context(), &req); err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OK(c)
|
||||
}
|
||||
|
||||
func (h *Business) AccountList(c *gin.Context) {
|
||||
|
||||
@@ -6,6 +6,10 @@ import (
|
||||
)
|
||||
|
||||
func RegisterBusiness(private *gin.RouterGroup, h *handler.Business) {
|
||||
private.GET("/sw/business/bindPlatform", h.BindingList)
|
||||
private.GET("/sw/business/:userId/bindPlatform", h.UserBindings)
|
||||
private.POST("/sw/business/bindPlatform", h.BindPlatform)
|
||||
private.DELETE("/sw/business/bindPlatform", h.UnbindPlatform)
|
||||
private.GET("/sw/business/list", h.AccountList)
|
||||
private.GET("/sw/business/:userId", h.Account)
|
||||
private.POST("/sw/business", h.CreateAccount)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type BindingUsecase interface {
|
||||
List(context.Context, business.BindingFilter, ...business.ListOption) ([]*business.Binding, int64, error)
|
||||
ForUser(context.Context, uint) ([]*business.Binding, error)
|
||||
Bind(context.Context, string, string, string) error
|
||||
Unbind(context.Context, business.PlatformKey) error
|
||||
}
|
||||
type BindingService struct{ uc BindingUsecase }
|
||||
|
||||
func NewBindingService(uc *business.BindingUsecase) *BindingService { return &BindingService{uc: uc} }
|
||||
|
||||
func localTime(t *time.Time) *string {
|
||||
if t == nil || t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
s := t.Format("2006-01-02 15:04:05")
|
||||
return &s
|
||||
}
|
||||
func bindingResponse(b *business.Binding) *dto.BusinessBindingResponse {
|
||||
return &dto.BusinessBindingResponse{UserID: b.UserID, SourceDB: b.SourceDB, PlatformName: business.PlatformName(b.SourceDB), PlatformBusierID: b.BusierID,
|
||||
Account: b.Account, UserName: b.UserName, PromoteCount: b.PromoteCount, Status: b.Status, RejectReason: b.RejectReason, CreateTime: localTime(&b.CreatedAt), ReviewTime: localTime(b.ReviewTime), ReviewAccount: b.ReviewAccount}
|
||||
}
|
||||
|
||||
func normalizePlatforms(values []string) ([]string, error) {
|
||||
result := []string{}
|
||||
seen := map[string]bool{}
|
||||
for _, raw := range values {
|
||||
for _, value := range strings.Split(raw, ",") {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
continue
|
||||
}
|
||||
value, err := business.NormalizePlatform(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !seen[value] {
|
||||
seen[value] = true
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
func (s *BindingService) List(ctx context.Context, req *dto.BusinessBindingQuery) (*dto.BusinessBindingPage, error) {
|
||||
if req == nil {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
sources, err := normalizePlatforms(req.SourceDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Status != "" && req.Status != "0" && req.Status != "2" && req.Status != "3" {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
page, size := req.Page, req.PageSize
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 10
|
||||
}
|
||||
if size > 500 {
|
||||
size = 500
|
||||
}
|
||||
if page > int(^uint(0)>>1)/size {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
rows, total, err := s.uc.List(ctx, business.BindingFilter{SourceDBs: sources, Account: req.Account, Status: req.Status}, business.ListOffset((page-1)*size), business.ListLimit(size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &dto.BusinessBindingPage{Rows: make([]*dto.BusinessBindingResponse, 0, len(rows)), Total: total}
|
||||
for _, b := range rows {
|
||||
out.Rows = append(out.Rows, bindingResponse(b))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (s *BindingService) ForUser(ctx context.Context, id uint) ([]*dto.BusinessBindingResponse, error) {
|
||||
if id == 0 {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
rows, err := s.uc.ForUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*dto.BusinessBindingResponse, 0, len(rows))
|
||||
for _, b := range rows {
|
||||
out = append(out, bindingResponse(b))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (s *BindingService) Bind(ctx context.Context, req *dto.BusinessBindingRequest) error {
|
||||
if req == nil || strings.TrimSpace(req.Account) == "" || utf8.RuneCountInString(req.Account) > 30 || strings.TrimSpace(req.Password) == "" || utf8.RuneCountInString(req.Password) > 100 {
|
||||
return business.ErrAccountInput
|
||||
}
|
||||
source, err := business.NormalizePlatform(req.SourceDB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.uc.Bind(ctx, source, req.Account, req.Password)
|
||||
}
|
||||
func (s *BindingService) Unbind(ctx context.Context, req *dto.BusinessBindingRequest) error {
|
||||
if req == nil || req.PlatformBusierID == 0 {
|
||||
return business.ErrAccountInput
|
||||
}
|
||||
source, err := business.NormalizePlatform(req.SourceDB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.uc.Unbind(ctx, business.PlatformKey{SourceDB: source, BusierID: req.PlatformBusierID})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package dto
|
||||
|
||||
type BusinessBindingQuery struct {
|
||||
Page int `form:"pageNum"`
|
||||
PageSize int `form:"pageSize"`
|
||||
SourceDB []string `form:"sourceDb"`
|
||||
Account string `form:"platformBusierAccount"`
|
||||
Status string `form:"status"`
|
||||
}
|
||||
type BusinessBindingRequest struct {
|
||||
SourceDB string `json:"sourceDb"`
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
PlatformBusierID uint64 `json:"platformBusierId"`
|
||||
}
|
||||
type BusinessBindingResponse struct {
|
||||
UserID uint `json:"userId"`
|
||||
SourceDB string `json:"sourceDb"`
|
||||
PlatformName string `json:"platformName"`
|
||||
PlatformBusierID uint64 `json:"platformBusierId"`
|
||||
Account string `json:"platformBusierAccount"`
|
||||
UserName string `json:"bindUserName"`
|
||||
PromoteCount *int64 `json:"promoteCount"`
|
||||
Status string `json:"status"`
|
||||
RejectReason *string `json:"rejectReason"`
|
||||
CreateTime *string `json:"createTime"`
|
||||
ReviewTime *string `json:"reviewTime"`
|
||||
ReviewAccount *string `json:"reviewAccount"`
|
||||
}
|
||||
type BusinessBindingPage struct {
|
||||
Rows []*BusinessBindingResponse `json:"rows"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
var ProviderSet = wire.NewSet(
|
||||
businessservice.NewUserPreferenceService,
|
||||
businessservice.NewAccountService,
|
||||
businessservice.NewBindingService,
|
||||
systemservice.ProviderSet,
|
||||
paymentservice.ProviderSet,
|
||||
integrationservice.ProviderSet,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""请求运行中的商务接口并生成脱敏结果;令牌从仓库外文件读取。"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base-url", default="http://127.0.0.1:18000")
|
||||
parser.add_argument("--token-file", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
token = args.token_file.read_text(encoding="utf-8").strip()
|
||||
results = []
|
||||
|
||||
def request(method, path, payload=None, expected=0):
|
||||
started = time.monotonic()
|
||||
req = urllib.request.Request(
|
||||
args.base_url.rstrip("/") + path,
|
||||
data=json.dumps(payload).encode() if payload is not None else None,
|
||||
headers={"x-token": token, "Content-Type": "application/json"}, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as response:
|
||||
body = json.load(response)
|
||||
item = {"method": method, "path": path, "http": response.status,
|
||||
"code": body.get("code"), "msg": body.get("msg"),
|
||||
"elapsed_ms": round((time.monotonic() - started) * 1000)}
|
||||
item["passed"] = response.status == 200 and body.get("code") == expected
|
||||
data = body.get("data")
|
||||
if isinstance(data, dict) and "rows" in data:
|
||||
item.update(total=data.get("total"), rows=len(data["rows"]))
|
||||
results.append(item)
|
||||
print(json.dumps(item, ensure_ascii=False), flush=True)
|
||||
return data if item["passed"] else None
|
||||
except (urllib.error.URLError, TimeoutError, ValueError) as error:
|
||||
item = {"method": method, "path": path, "passed": False, "error": str(error)}
|
||||
results.append(item)
|
||||
print(json.dumps(item, ensure_ascii=False), flush=True)
|
||||
return None
|
||||
|
||||
first = request("GET", "/sw/business/list?pageNum=1&pageSize=500")
|
||||
request("GET", "/sw/business/list?pageNum=2&pageSize=10")
|
||||
if first and first["rows"]:
|
||||
account = first["rows"][0]
|
||||
request("GET", f"/sw/business/{account['userId']}")
|
||||
request("GET", "/sw/business/list?" + urllib.parse.urlencode({"userName": account["userName"]}))
|
||||
bound = next((row for row in first["rows"] if row["bindCount"] > 0), account)
|
||||
request("GET", f"/sw/business/{bound['userId']}/bindPlatform")
|
||||
request("GET", "/sw/user/preference")
|
||||
request("GET", "/sw/business/bindPlatform?pageNum=1&pageSize=10")
|
||||
request("GET", "/sw/business/bindPlatform?sourceDb=lx,hw&status=0&pageSize=2")
|
||||
# 使用校验必然拒绝的请求体检查写接口,不新增或修改测试库业务记录。
|
||||
for method, path in [("PUT", "/sw/user/preference"), ("POST", "/sw/business"),
|
||||
("PUT", "/sw/business"), ("POST", "/sw/business/bindPlatform"),
|
||||
("DELETE", "/sw/business/bindPlatform")]:
|
||||
request(method, path, {}, expected=7)
|
||||
args.output.write_text(json.dumps({"base_url": args.base_url, "checks": results,
|
||||
"passed": all(row["passed"] for row in results)}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
if not all(row["passed"] for row in results):
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -7,3 +7,8 @@ export const listBusiness = (params) => service({ url: '/sw/business/list', meth
|
||||
export const getBusiness = (id) => service({ url: `/sw/business/${id}`, method: 'get' })
|
||||
export const createBusiness = (data) => service({ url: '/sw/business', method: 'post', data })
|
||||
export const updateBusiness = (data) => service({ url: '/sw/business', method: 'put', data })
|
||||
|
||||
export const listBusinessBindings = (params) => service({ url: '/sw/business/bindPlatform', method: 'get', params })
|
||||
export const listUserBindings = (id) => service({ url: `/sw/business/${id}/bindPlatform`, method: 'get' })
|
||||
export const bindPlatform = (data) => service({ url: '/sw/business/bindPlatform', method: 'post', data })
|
||||
export const unbindPlatform = (data) => service({ url: '/sw/business/bindPlatform', method: 'delete', data })
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"/src/view/superAdmin/position/position.vue": "Position",
|
||||
"/src/view/superAdmin/user/user.vue": "User",
|
||||
"/src/view/sw/BusinessList.vue": "SwBusinessList",
|
||||
"/src/view/sw/components/BusinessBindings.vue": "SwBusinessBindings",
|
||||
"/src/view/system/security/forceChangePassword.vue": "ForceChangePassword",
|
||||
"/src/view/system/security/index.vue": "SecurityConfig",
|
||||
"/src/view/system/state.vue": "State",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="kra-search-box">
|
||||
<el-tabs v-model="activeTab"><el-tab-pane label="商务列表" name="accounts" /><el-tab-pane label="游戏 SDK 绑定" name="bindings" /></el-tabs>
|
||||
<business-bindings v-if="activeTab === 'bindings'" @changed="load" />
|
||||
<div v-show="activeTab === 'accounts'" class="kra-search-box">
|
||||
<el-form :inline="true" :model="query" @submit.prevent="search">
|
||||
<el-form-item label="商务账号"><el-input v-model="query.userName" clearable placeholder="请输入账号" /></el-form-item>
|
||||
<el-form-item label="姓名"><el-input v-model="query.realName" clearable placeholder="请输入姓名" /></el-form-item>
|
||||
@@ -17,7 +19,7 @@
|
||||
<el-form-item><el-button type="primary" icon="search" native-type="submit">查询</el-button><el-button icon="refresh" @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="kra-table-box">
|
||||
<div v-show="activeTab === 'accounts'" class="kra-table-box">
|
||||
<div class="kra-btn-list">
|
||||
<el-button v-if="canAdd" type="primary" icon="plus" @click="openCreate">添加子商务</el-button>
|
||||
</div>
|
||||
@@ -27,7 +29,7 @@
|
||||
<el-table-column prop="parentUserName" label="上级商务" min-width="140" />
|
||||
<el-table-column prop="realName" label="姓名" min-width="100" />
|
||||
<el-table-column prop="phoneNumber" label="手机号" min-width="130" />
|
||||
<el-table-column prop="bindCount" label="SDK 商务数" width="120" />
|
||||
<el-table-column prop="bindCount" label="SDK 商务数" width="120"><template #default="{ row }"><el-button v-if="row.bindCount > 0" link type="primary" @click="showBindings(row)">{{ row.bindCount }}</el-button><span v-else>0</span></template></el-table-column>
|
||||
<el-table-column prop="promoteCount" label="直属渠道数" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.promoteCount === null" content="当前数据库尚未接入渠道数据,商务资料和绑定数仍可查看。"><span class="text-gray-400">未接入</span></el-tooltip>
|
||||
@@ -42,6 +44,7 @@
|
||||
<el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" />
|
||||
</div>
|
||||
</div>
|
||||
<el-drawer v-model="bindingsDrawer" title="已通过的游戏 SDK 绑定" size="85%" destroy-on-close><business-bindings :user-id="bindingUserId" /></el-drawer>
|
||||
<el-drawer v-model="drawer" :title="editing ? '编辑商务' : '添加子商务'" :size="480" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||
<el-form-item label="商务账号" prop="userName"><el-input v-model="form.userName" maxlength="30" /></el-form-item>
|
||||
@@ -67,9 +70,14 @@ import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useBtnAuth } from '@/utils/btnAuth'
|
||||
import { listBusiness, getBusiness, createBusiness, updateBusiness } from '@/api/business'
|
||||
import BusinessBindings from './components/BusinessBindings.vue'
|
||||
|
||||
defineOptions({ name: 'SwBusinessList' })
|
||||
const user = useUserStore()
|
||||
const activeTab = ref('accounts')
|
||||
const bindingsDrawer = ref(false)
|
||||
const bindingUserId = ref(0)
|
||||
const showBindings = (row) => { bindingUserId.value = row.userId; bindingsDrawer.value = true }
|
||||
const buttons = useBtnAuth()
|
||||
const levels = { 1: '一级商务', 2: '二级商务', 3: '三级商务' }
|
||||
const statuses = { 0: '正常', 1: '停用', 2: '已驳回', 3: '待审核' }
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!userId" class="kra-search-box">
|
||||
<el-form :inline="true" :model="query" @submit.prevent="search">
|
||||
<el-form-item label="所属平台"><el-select v-model="query.sourceDb" multiple clearable placeholder="全部平台" style="width: 220px"><el-option v-for="(label, value) in platforms" :key="value" :value="value" :label="label" /></el-select></el-form-item>
|
||||
<el-form-item label="游戏商务账号"><el-input v-model="query.platformBusierAccount" clearable /></el-form-item>
|
||||
<el-form-item label="状态"><el-select v-model="query.status" clearable style="width: 130px"><el-option v-for="(label, value) in statuses" :key="value" :value="value" :label="label" /></el-select></el-form-item>
|
||||
<el-form-item><el-button type="primary" icon="search" native-type="submit">查询</el-button><el-button icon="refresh" @click="reset">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="kra-table-box">
|
||||
<div v-if="!userId && canBind" class="kra-btn-list"><el-button type="primary" icon="plus" @click="openBind">绑定游戏商务</el-button></div>
|
||||
<el-table v-loading="loading" :data="rows" :row-key="rowKey">
|
||||
<el-table-column prop="platformName" label="平台" min-width="80" />
|
||||
<el-table-column prop="platformBusierAccount" label="游戏商务账号" min-width="140" />
|
||||
<el-table-column prop="bindUserName" label="绑定商务" min-width="140" />
|
||||
<el-table-column label="直属渠道数" min-width="110"><template #default="{ row }"><span :class="{ 'text-gray-400': row.promoteCount === null }">{{ row.promoteCount === null ? '未接入' : row.promoteCount }}</span></template></el-table-column>
|
||||
<el-table-column label="状态" width="110"><template #default="{ row }"><el-tag :type="row.status === '0' ? 'success' : row.status === '2' ? 'danger' : 'warning'">{{ statuses[row.status] || row.status }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="createTime" label="提交时间" min-width="180" />
|
||||
<el-table-column prop="reviewTime" label="审核时间" min-width="180" />
|
||||
<el-table-column prop="reviewAccount" label="审核人" min-width="100" />
|
||||
<el-table-column prop="rejectReason" label="驳回原因" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column v-if="canBind" label="操作" width="90" fixed="right"><template #default="{ row }"><el-button v-if="row.userId === user.userInfo.ID" link type="danger" @click="remove(row)">解绑</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<div v-if="!userId" class="kra-pagination"><el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" /></div>
|
||||
</div>
|
||||
<el-dialog v-model="dialog" title="绑定游戏商务" width="460px" destroy-on-close>
|
||||
<el-alert title="验证成功后提交 OA 审核,审核通过后可查看对应渠道数据。" type="info" :closable="false" class="mb-4" />
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||
<el-form-item label="所属平台" prop="sourceDb"><el-select v-model="form.sourceDb"><el-option v-for="(label, value) in platforms" :key="value" :value="value" :label="label" /></el-select></el-form-item>
|
||||
<el-form-item label="游戏商务账号" prop="account"><el-input v-model="form.account" maxlength="30" autocomplete="off" /></el-form-item>
|
||||
<el-form-item label="游戏商务密码" prop="password"><el-input v-model="form.password" type="password" show-password autocomplete="new-password" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialog = false">取消</el-button><el-button type="primary" :loading="saving" @click="submit">验证并提交</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useBtnAuth } from '@/utils/btnAuth'
|
||||
import { listBusinessBindings, listUserBindings, bindPlatform, unbindPlatform } from '@/api/business'
|
||||
|
||||
defineOptions({ name: 'SwBusinessBindings' })
|
||||
const props = defineProps({ userId: { type: Number, default: 0 } })
|
||||
const emit = defineEmits(['changed'])
|
||||
const user = useUserStore()
|
||||
const buttons = useBtnAuth()
|
||||
const canBind = computed(() => Number(user.userInfo.level) === 3 && Boolean(buttons['sw:business:bind']))
|
||||
const platforms = { lx: '龙翔', hw: '汇玩', yq: '游趣' }
|
||||
const statuses = { 0: '已通过', 2: '已驳回', 3: '待审核' }
|
||||
const query = reactive({ pageNum: 1, pageSize: 10, sourceDb: [], platformBusierAccount: '', status: '' })
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const dialog = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRef = ref()
|
||||
const form = ref({})
|
||||
const rules = Object.fromEntries(['sourceDb', 'account', 'password'].map((key) => [key, [{ required: true, message: '请填写完整信息', trigger: 'blur' }]]))
|
||||
const rowKey = (row) => `${row.userId}:${row.sourceDb}:${row.platformBusierId}`
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = props.userId ? await listUserBindings(props.userId) : await listBusinessBindings({ ...query, sourceDb: query.sourceDb.join(',') })
|
||||
if (res.code === 0) {
|
||||
rows.value = props.userId ? res.data : res.data.rows
|
||||
total.value = props.userId ? rows.value.length : res.data.total
|
||||
}
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
const search = () => { query.pageNum = 1; return load() }
|
||||
const reset = () => { Object.assign(query, { sourceDb: [], platformBusierAccount: '', status: '' }); return search() }
|
||||
const openBind = () => { form.value = { sourceDb: 'lx', account: '', password: '' }; dialog.value = true }
|
||||
const submit = async () => {
|
||||
if (!await formRef.value.validate().catch(() => false)) return
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await bindPlatform(form.value)
|
||||
if (res.code === 0) { ElMessage.success('已提交,请等待 OA 审核'); dialog.value = false; form.value.password = ''; emit('changed'); await load() }
|
||||
} finally { saving.value = false }
|
||||
}
|
||||
const remove = async (row) => {
|
||||
if (!await ElMessageBox.confirm(`确认解绑 ${row.platformName} 的游戏商务 ${row.platformBusierAccount}?`, '解绑确认', { type: 'warning' }).then(() => true).catch(() => false)) return
|
||||
const res = await unbindPlatform({ sourceDb: row.sourceDb, platformBusierId: row.platformBusierId })
|
||||
if (res.code === 0) { ElMessage.success('解绑成功'); emit('changed'); await load() }
|
||||
}
|
||||
watch(() => props.userId, load, { immediate: true })
|
||||
</script>
|
||||
Reference in New Issue
Block a user