feat: 迁移商务与游戏商务画像及交互看板

This commit is contained in:
Yvan
2026-11-03 23:11:46 +08:00
parent 7f8105f6fb
commit 0de822f121
30 changed files with 1959 additions and 26 deletions
+68
View File
@@ -0,0 +1,68 @@
"""只读验证运行实例的商务画像、缺数据处理与参数校验。"""
import argparse
import json
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()
checks = []
def get(path, expected=0):
request = urllib.request.Request(args.base_url + path, headers={"x-token": token})
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.load(response)
check = {"path": path, "http": response.status, "code": payload.get("code"),
"msg": payload.get("msg"), "passed": response.status == 200 and payload.get("code") == expected}
data = payload.get("data", {})
if isinstance(data, dict) and "basic" in data:
check.update(available=data.get("available"), promote_count=data["basic"].get("promoteCount"),
register_points=len(data.get("registerTrend", [])), pay_points=len(data.get("payTrend", [])))
checks.append(check)
return data
accounts = get("/sw/business/list?pageNum=1&pageSize=500")["rows"]
def bound_subtree(account):
prefix = f"{account.get('idPath') or '0'},{account['userId']}"
ids = [account]
if int(account.get("level") or 0) < 3:
ids += [row for row in accounts if row["userId"] != account["userId"] and
(row.get("parentId") == account["userId"] or row.get("idPath") == prefix or
str(row.get("idPath") or "").startswith(prefix + ","))]
return sum(row.get("bindCount") or 0 for row in ids)
empty = next((row for row in accounts if bound_subtree(row) == 0), None)
bound = next((row for row in accounts if bound_subtree(row) > 0), None)
if empty:
data = get(f"/sw/business/{empty['userId']}/portrait?sourceDbList=lx,hw,yq&period=today")
checks[-1]["passed"] &= data.get("available") is True and data.get("metrics", {}).get("register", {}).get("value") == "0" and len(data.get("registerTrend", [])) == 24
data = get(f"/sw/business/{empty['userId']}/portrait?sourceDbList=lx&period=thisMonth")
checks[-1]["passed"] &= data.get("period") == "thismonth" and 1 <= len(data.get("registerTrend", [])) <= 31
if bound:
data = get(f"/sw/business/{bound['userId']}/portrait?sourceDbList=lx,hw,yq&period=last7")
if data.get("available") is False:
checks[-1]["passed"] &= data.get("metrics") is None and bool(data.get("unavailableReason"))
else:
checks[-1]["passed"] &= len(data.get("registerTrend", [])) == 7
if accounts:
get(f"/sw/business/{accounts[0]['userId']}/portrait", expected=7)
get("/sw/business/bindPlatform/portrait?sourceDb=lx&platformBusierId=0", expected=7)
report = {"scope": "真实 sw7 登录会话,只读请求;有绑定但业务表未接入时不伪造零,无绑定时验证真实空画像。完整指标和 SDK 基本信息通过隔离双库 TCP HTTP 测试。",
"checks": checks, "passed": all(check["passed"] for check in checks)}
args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, ensure_ascii=False))
if not report["passed"]:
raise SystemExit(1)
if __name__ == "__main__":
main()
+101
View File
@@ -0,0 +1,101 @@
// 真实 Chrome 核对三个画像入口、统计切换及嵌套抽屉;所有业务数据使用受控响应。
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
const { chromium } = require(process.env.SW_PLAYWRIGHT_MODULE || 'playwright')
;(async () => {
const token = (await fs.readFile(path.join(process.env.TEMP, 'sw-kra-http-test-token'), 'utf8')).trim()
const browser = await chromium.launch({ channel: 'chrome', headless: true })
const results = []
try {
for (const level of [1, 3]) {
const context = await browser.newContext({ viewport: { width: 1600, height: 1080 } })
await context.addInitScript(value => localStorage.setItem('token', value), token)
const page = await context.newPage()
const errors = [], portraits = [], alertQueries = [], channelQueries = []
let unavailable = false
const normal = { userId: 70009, sourceDb: 'lx', platformName: '龙翔', platformBusierId: 14, platformBusierAccount: 'SDK测试账号', bindUserName: '测试商务', status: '0', createTime: '2026-11-01 10:00:00', promoteCount: 1 }
page.on('pageerror', error => errors.push(error.message))
await page.route('http://127.0.0.1:8080/api/**', async route => {
const request = route.request(), url = new URL(request.url()), api = url.pathname.slice(4)
const ok = data => route.fulfill({ json: { code: 0, msg: '成功', data } })
if (api === '/sw/business/list') return ok({ rows: level === 3 ? [] : [{ userId: 70009, userName: '测试商务账号', realName: '测试商务', level: 3, status: '0', bindCount: 1, promoteCount: 1, createTime: '2026-11-01 10:00:00' }], total: level === 3 ? 0 : 1 })
if (api === '/sw/business/70009/bindPlatform') return ok([normal])
if (api === '/sw/business/bindPlatform') return ok({ rows: [normal, { ...normal, platformBusierId: 15, platformBusierAccount: '驳回账号', status: '2' }, { ...normal, platformBusierId: 16, platformBusierAccount: '待审账号', status: '3' }], total: 3 })
if (api === '/sw/business/70009/portrait' || api === '/sw/business/bindPlatform/portrait') {
const sdk = api.includes('bindPlatform')
const period = url.searchParams.get('period') || 'today'
portraits.push({ api, period, source: url.searchParams.get(sdk ? 'sourceDb' : 'sourceDbList'), platformBusierId: url.searchParams.get('platformBusierId') })
const length = ['today', 'yesterday'].includes(period) ? 24 : 7
const trends = Array.from({ length }, (_, i) => ({ label: length === 24 ? String(i).padStart(2, '0') + ':00' : new Date(Date.UTC(2026, 9, 28 + i)).toISOString().slice(5, 10), value: i === 1 ? '2' : '0' }))
const compareLabel = period === 'today' ? '较昨日' : period === 'yesterday' ? '较前日' : period === 'thismonth' ? '较上月同期' : '较上一周期'
const metric = value => ({ value, compareValue: '1', changeRate: '100.0', changeDiff: null, compareLabel })
return ok({ basic: { userId: sdk ? 14 : 70009, userName: sdk ? 'SDK测试账号' : '测试商务账号', realName: '画像姓名', level: 3, levelLabel: '三级商务', parentUserName: '上级账号', status: '0', statusLabel: sdk ? '待审核' : '正常', promoteCount: unavailable ? null : 1, monthNewPromote: unavailable ? null : 1, createTime: '2026-11-01 10:00:00' }, period,
available: !unavailable, unavailableReason: unavailable ? '渠道数据尚未接入,画像指标暂不可用' : '', metrics: unavailable ? null : { register: metric('2'), payAmount: metric('12.00'), payRate: { ...metric('50.0'), changeRate: null, changeDiff: '25.0', compareValue: '25.0' }, arpu: metric('6.00') },
registerTrend: unavailable ? [] : trends, payTrend: unavailable ? [] : trends.map(point => ({ ...point, value: point.value === '2' ? '12.00' : '0.00' })), registerTop5: unavailable ? [] : [{ rank: 1, sourceDbLabel: '龙翔', promoteId: 100, promoteName: '测试主渠道', value: '2.00' }], payTop5: unavailable ? [] : [{ rank: 1, sourceDbLabel: '龙翔', promoteId: 100, promoteName: '测试主渠道', value: '12.00' }] })
}
if (api === '/sw/alertRecord/list') { alertQueries.push(Object.fromEntries(url.searchParams)); return ok({ available: true, pageData: { rows: [{ id: '1', createdAt: '2026-11-03 10:00:00', targetPromoteName: '测试预警渠道', ruleType: 'register_anomaly', alertContent: '测试关联预警', status: 1, statusLabel: '未处理' }], total: 1 }, statusCount: { 1: 1 } }) }
if (api === '/sw/business/70009/promotes') { channelQueries.push({ api, ...Object.fromEntries(url.searchParams) }); return ok({ rows: [{ promoteId: 100, promoteName: '测试直属渠道', sourceDb: 'lx', platformName: '龙翔' }], total: 1, available: true }) }
const response = await route.fetch({ url: 'http://127.0.0.1:18000' + api + url.search, headers: { ...request.headers(), 'x-token': token } })
if (api === '/user/getUserInfo') { const body = await response.json(); Object.assign(body.data.userInfo, { ID: level === 3 ? 70009 : 70001, level, authorityId: 900 + level, nickName: '画像核对账号', phone: '', email: '' }); return route.fulfill({ response, json: body }) }
return route.fulfill({ response })
})
await page.goto('http://127.0.0.1:8080/#/layout/sw-BusMgmt/BusinessList')
const close = async dialog => { await dialog.getByRole('button', { name: '关闭', exact: true }).click(); await dialog.waitFor({ state: 'hidden' }) }
if (level === 1) {
await page.getByRole('button', { name: '画像', exact: true }).click()
const portrait = page.getByRole('dialog', { name: '商务画像 — 测试商务', exact: true })
await portrait.getByText('画像姓名', { exact: true }).waitFor()
await portrait.locator('.el-loading-mask').first().waitFor({ state: 'hidden' })
await portrait.getByText('测试关联预警', { exact: true }).waitFor()
assert.equal(portraits.at(-1).source, 'lx,hw,yq')
await portrait.getByRole('img', { name: '注册量趋势', exact: true }).focus()
await portrait.getByRole('img', { name: '注册量趋势', exact: true }).press('ArrowRight')
await portrait.locator('.trend-tooltip').getByText('01:00', { exact: true }).waitFor()
await portrait.getByText('近7天', { exact: true }).click()
await page.waitForFunction(() => [...document.querySelectorAll('.profile-two-columns .el-card__header')].some(node => node.textContent.includes('近7天注册量')))
assert.equal(portraits.at(-1).period, 'last7')
await portrait.locator('.el-loading-mask').first().waitFor({ state: 'hidden' })
await page.screenshot({ path: path.join(process.env.TEMP, 'sw-business-portrait.png'), animations: 'disabled' })
await close(portrait)
await page.getByRole('button', { name: '查看SDK商务绑定详情', exact: true }).click()
const bindings = page.getByRole('dialog', { name: 'SDK商务绑定详情', exact: true })
await bindings.getByRole('button', { name: '龙翔', exact: true }).click()
const sdk = page.getByRole('dialog', { name: '游戏商务画像 — SDK测试账号', exact: true })
await sdk.getByText('画像姓名', { exact: true }).waitFor()
assert.equal(portraits.at(-1).platformBusierId, '14')
assert.equal(alertQueries.at(-1).businessId, '70009')
assert.equal(alertQueries.at(-1).platformCodes, 'lx')
await sdk.locator('.profile-info').getByText('待审核', { exact: true }).waitFor()
assert.equal(await sdk.locator('.profile-info .el-tag--success').count(), 0)
await sdk.locator('.profile-info').getByRole('button', { name: '1', exact: true }).click()
const channels = page.getByRole('dialog', { name: '旗下渠道', exact: true })
await channels.waitFor(); await channels.locator('.el-loading-mask').waitFor({ state: 'hidden' })
assert.equal(channelQueries.at(-1).api, '/sw/business/70009/promotes')
assert.equal(channelQueries.at(-1).platformBusierId, '14')
await channels.locator('.el-drawer__close-btn').click(); await channels.waitFor({ state: 'hidden' })
await close(sdk)
await bindings.locator('.el-drawer__close-btn').click(); await bindings.waitFor({ state: 'hidden' })
unavailable = true
await page.getByRole('button', { name: '画像', exact: true }).click()
await portrait.getByText('渠道数据尚未接入,画像指标暂不可用', { exact: true }).waitFor()
assert.equal(await portrait.getByRole('img', { name: '注册量趋势', exact: true }).count(), 0)
await close(portrait)
} else {
await page.getByRole('tab', { name: '绑定列表', exact: true }).click()
const normalRow = page.locator('.el-table__body-wrapper tr').filter({ hasText: 'SDK测试账号' })
await normalRow.getByRole('button', { name: '画像', exact: true }).click()
const sdk = page.getByRole('dialog', { name: '游戏商务画像 — SDK测试账号', exact: true })
await sdk.getByText('画像姓名', { exact: true }).waitFor()
await close(sdk)
assert.equal(await page.locator('.el-table__body-wrapper tr').filter({ hasText: '驳回账号' }).getByRole('button', { name: '画像', exact: true }).count(), 0)
assert.equal(await page.locator('.el-table__body-wrapper tr').filter({ hasText: '待审账号' }).getByRole('button', { name: '画像', exact: true }).count(), 0)
}
assert.deepEqual(errors, [])
const result = { level, passed: true, entry: level === 1 ? '商务列表与只读 SDK 详情' : '三级绑定列表正常行', pageErrors: errors }
results.push(result); console.log(JSON.stringify(result)); await context.close()
}
await fs.writeFile('docs/sw6-to-kra/portrait-page-2026-11-03.json', JSON.stringify({ method: '真实 Chrome 渲染;业务接口使用受控数据。验证三个入口、时间切换、交互图表、SDK 状态、SW 用户 ID 与平台 ID 分离、缺数据提示。', results }, null, 2) + '\n')
} finally { await browser.close() }
})().catch(error => { console.error(error); process.exitCode = 1 })