feat: 迁移商务与游戏商务画像及交互看板
This commit is contained in:
@@ -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()
|
||||
@@ -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 })
|
||||
Reference in New Issue
Block a user