更新
This commit is contained in:
@@ -4,7 +4,7 @@ import ts from '../../admin/node_modules/typescript/lib/typescript.js'
|
||||
|
||||
const source = fs.readFileSync(new URL('../../admin/src/views/first_visit/wecom_promotion/components/promotion-automation.ts', import.meta.url), 'utf8')
|
||||
const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText
|
||||
const { defaultAutomationConfig, cloneAutomationConfig, validateAutomationConfig, validateCustomTagName, validateWelcomeMessage, welcomeScheduleOverlap, previewTemplate } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`)
|
||||
const { defaultAutomationConfig, cloneAutomationConfig, serializeAutomationConfig, validateAutomationConfig, validateCustomTagName, validateWelcomeMessage, welcomeScheduleOverlap, previewTemplate } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`)
|
||||
|
||||
const defaults = defaultAutomationConfig()
|
||||
assert.equal(validateAutomationConfig(defaults, [1]), '')
|
||||
@@ -19,6 +19,16 @@ assert.equal(validateAutomationConfig(configured, [1]), '')
|
||||
const copy = cloneAutomationConfig(configured)
|
||||
assert.equal(copy.backup_userids, undefined)
|
||||
assert.equal(copy.reception_schedule[0].member_userids, undefined)
|
||||
assert.deepEqual(
|
||||
Object.fromEntries(Object.entries(serializeAutomationConfig({
|
||||
...copy,
|
||||
tags_enabled: true,
|
||||
remark_enabled: false,
|
||||
description_enabled: true,
|
||||
welcome_schedule_enabled: false
|
||||
})).filter(([key]) => key.endsWith('_enabled'))),
|
||||
{ tags_enabled: 1, remark_enabled: 0, description_enabled: 1, welcome_schedule_enabled: 0 }
|
||||
)
|
||||
copy.reception_schedule[0].weekdays.push(2)
|
||||
assert.deepEqual(configured.reception_schedule[0].weekdays, [1])
|
||||
assert.match(validateAutomationConfig({ ...configured, backup_member_admin_ids: [] }, [1]), /备用/)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\auth\MenuLogic;
|
||||
use app\common\service\qywx\QywxPromotionOperatorAccess;
|
||||
use think\facade\Db;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
$pool = Db::name('qywx_promotion_pool')->whereNull('delete_time')->order('id', 'asc')->find();
|
||||
$grantorId = (int) (Db::name('admin')->where('root', 1)->whereNull('delete_time')->value('id') ?? 0);
|
||||
if (!$pool || $grantorId <= 0) {
|
||||
throw new RuntimeException('缺少分流方案或 root 管理员,无法验证动态共享权限');
|
||||
}
|
||||
|
||||
$candidate = null;
|
||||
foreach (Db::name('admin')
|
||||
->where('root', 0)
|
||||
->where('disable', 0)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray() as $admin) {
|
||||
$adminId = (int) ($admin['id'] ?? 0);
|
||||
if ($adminId > 0
|
||||
&& !QywxPromotionOperatorAccess::hasBasePagePermission($adminId, $admin)
|
||||
&& !QywxPromotionOperatorAccess::hasSharedPagePermission($adminId)) {
|
||||
$candidate = $admin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($candidate === null) {
|
||||
throw new RuntimeException('未找到无基础页面权限且无现有共享的启用账号');
|
||||
}
|
||||
|
||||
$candidateId = (int) $candidate['id'];
|
||||
$poolId = (int) $pool['id'];
|
||||
$now = time();
|
||||
Db::startTrans();
|
||||
try {
|
||||
$relation = Db::name('qywx_promotion_pool_operator')
|
||||
->where('pool_id', $poolId)
|
||||
->where('admin_id', $candidateId)
|
||||
->find();
|
||||
if ($relation) {
|
||||
Db::name('qywx_promotion_pool_operator')->where('id', (int) $relation['id'])->update([
|
||||
'granted_by_admin_id' => $grantorId,
|
||||
'delete_time' => null,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
} else {
|
||||
Db::name('qywx_promotion_pool_operator')->insert([
|
||||
'pool_id' => $poolId,
|
||||
'admin_id' => $candidateId,
|
||||
'granted_by_admin_id' => $grantorId,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
'delete_time' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!QywxPromotionOperatorAccess::hasSharedPagePermission($candidateId)
|
||||
|| !QywxPromotionOperatorAccess::hasPagePermission($candidateId, $candidate)) {
|
||||
throw new RuntimeException('共享关系未生成动态页面权限');
|
||||
}
|
||||
if (QywxPromotionOperatorAccess::visibleAdminIds($candidateId, $candidate) !== []) {
|
||||
throw new RuntimeException('纯共享账号错误继承了普通角色数据范围');
|
||||
}
|
||||
if (!in_array($poolId, QywxPromotionOperatorAccess::activePoolIds($candidateId), true)) {
|
||||
throw new RuntimeException('共享方案未进入操作人专用数据范围');
|
||||
}
|
||||
if (!in_array(QywxPromotionOperatorAccess::PAGE_PERMISSION, AuthLogic::getAuthByAdminId($candidateId), true)) {
|
||||
throw new RuntimeException('共享账号的接口权限列表缺少获客助手页面权限');
|
||||
}
|
||||
$menuJson = json_encode(MenuLogic::getMenuByAdminId($candidateId), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($menuJson) || !str_contains($menuJson, QywxPromotionOperatorAccess::PAGE_PERMISSION)) {
|
||||
throw new RuntimeException('共享账号的导航菜单缺少获客助手页面');
|
||||
}
|
||||
} finally {
|
||||
Db::rollback();
|
||||
}
|
||||
|
||||
if (QywxPromotionOperatorAccess::hasSharedPagePermission($candidateId)) {
|
||||
throw new RuntimeException('测试事务回滚后仍残留共享权限');
|
||||
}
|
||||
|
||||
echo sprintf(
|
||||
"WECOM_PROMOTION_OPERATOR_DYNAMIC_ACCESS_OK admin=%d pool=%d\n",
|
||||
$candidateId,
|
||||
$poolId
|
||||
);
|
||||
@@ -32,6 +32,16 @@ foreach ($overview['member_options'] as $member) {
|
||||
throw new RuntimeException('member_options 缺少树形下拉展示部门');
|
||||
}
|
||||
}
|
||||
$enabledOperatorCount = 0;
|
||||
foreach ($overview['operator_options'] as $operator) {
|
||||
$isEnabled = (int) ($operator['disable'] ?? 0) === 0;
|
||||
if ($isEnabled) {
|
||||
$enabledOperatorCount++;
|
||||
}
|
||||
if ((bool) ($operator['can_grant'] ?? false) !== $isEnabled) {
|
||||
throw new RuntimeException('操作人可授权状态不应依赖目标账号预先拥有页面权限');
|
||||
}
|
||||
}
|
||||
$memberIds = array_values(array_unique(array_map('intval', array_column($overview['member_options'], 'id'))));
|
||||
if ($memberIds !== []) {
|
||||
$disabledMemberCount = (int) Db::name('admin')->whereIn('id', $memberIds)->where('disable', '<>', 0)->count();
|
||||
@@ -83,10 +93,11 @@ if ($scopedAdmin) {
|
||||
}
|
||||
|
||||
echo sprintf(
|
||||
"WECOM_PROMOTION_OVERVIEW_SMOKE_OK configured=%d callback=%d pools=%d links=%d members=%d\n",
|
||||
"WECOM_PROMOTION_OVERVIEW_SMOKE_OK configured=%d callback=%d pools=%d links=%d members=%d operators=%d\n",
|
||||
!empty($overview['config']['ready']) ? 1 : 0,
|
||||
!empty($overview['config']['callback_ready']) ? 1 : 0,
|
||||
count($overview['pools']),
|
||||
count($overview['links']),
|
||||
count($overview['member_options'])
|
||||
count($overview['member_options']),
|
||||
$enabledOperatorCount
|
||||
);
|
||||
|
||||
@@ -6,12 +6,27 @@ $root = dirname(__DIR__, 2);
|
||||
$logicPath = __DIR__ . '/../app/adminapi/logic/firstvisit/WecomPromotionLogic.php';
|
||||
$customerLogicPath = __DIR__ . '/../app/adminapi/logic/firstvisit/WecomAcquisitionCustomerLogic.php';
|
||||
$controllerPath = __DIR__ . '/../app/adminapi/controller/firstvisit/WecomPromotionController.php';
|
||||
$operatorAccessPath = __DIR__ . '/../app/common/service/qywx/QywxPromotionOperatorAccess.php';
|
||||
$authLogicPath = __DIR__ . '/../app/adminapi/logic/auth/AuthLogic.php';
|
||||
$menuLogicPath = __DIR__ . '/../app/adminapi/logic/auth/MenuLogic.php';
|
||||
$authMiddlewarePath = __DIR__ . '/../app/adminapi/http/middleware/AuthMiddleware.php';
|
||||
$apiPath = $root . '/admin/src/api/first_visit.ts';
|
||||
$viewPath = $root . '/admin/src/views/first_visit/wecom_promotion/index.vue';
|
||||
$migrationPath = $root . '/server/sql/1.9.20260828/add_wecom_promotion_pool_operators.sql';
|
||||
|
||||
$sources = [];
|
||||
foreach (compact('logicPath', 'customerLogicPath', 'controllerPath', 'apiPath', 'viewPath', 'migrationPath') as $name => $path) {
|
||||
foreach (compact(
|
||||
'logicPath',
|
||||
'customerLogicPath',
|
||||
'controllerPath',
|
||||
'operatorAccessPath',
|
||||
'authLogicPath',
|
||||
'menuLogicPath',
|
||||
'authMiddlewarePath',
|
||||
'apiPath',
|
||||
'viewPath',
|
||||
'migrationPath'
|
||||
) as $name => $path) {
|
||||
$source = file_get_contents($path);
|
||||
if (!is_string($source)) {
|
||||
throw new RuntimeException("无法读取 {$name}: {$path}");
|
||||
@@ -27,7 +42,7 @@ foreach (['qywx_promotion_pool_operator', 'uk_pool_admin', 'granted_by_admin_id'
|
||||
}
|
||||
|
||||
$logic = $sources['logicPath'];
|
||||
foreach (['batchSetOperators', 'applyPoolAccessScope', 'isPoolOperator', 'can_manage_access', 'operator_options'] as $needle) {
|
||||
foreach (['batchSetOperators', 'applyPoolAccessScope', 'isPoolOperator', 'can_manage_access', 'operator_options', 'clearOperatorAuthCaches'] as $needle) {
|
||||
if (!str_contains($logic, $needle)) {
|
||||
throw new RuntimeException("获客助手共享权限逻辑缺少 {$needle}");
|
||||
}
|
||||
@@ -41,9 +56,50 @@ $deleteMethod = substr($logic, $deleteStart, $deleteEnd - $deleteStart);
|
||||
if (!str_contains($deleteMethod, "assertScopedRow('qywx_promotion_pool', \$id, \$adminId, \$adminInfo, false)")) {
|
||||
throw new RuntimeException('共享操作人不得永久删除分流方案');
|
||||
}
|
||||
if (!str_contains($deleteMethod, 'assertBasePagePermission($adminId, $adminInfo)')
|
||||
|| !str_contains($deleteMethod, 'clearOperatorAuthCaches($operatorAdminIds)')) {
|
||||
throw new RuntimeException('删除方案必须校验基础权限并清理共享账号权限缓存');
|
||||
}
|
||||
$batchStart = strpos($logic, 'public static function batchSetOperators(');
|
||||
$batchEnd = strpos($logic, 'private static function isRemoteLinkAlreadyMissing(', $batchStart === false ? 0 : $batchStart);
|
||||
if ($batchStart === false || $batchEnd === false) {
|
||||
throw new RuntimeException('无法定位 batchSetOperators');
|
||||
}
|
||||
$batchMethod = substr($logic, $batchStart, $batchEnd - $batchStart);
|
||||
foreach (['assertBasePagePermission($adminId, $adminInfo)', 'clearOperatorAuthCaches($operatorAdminIds)'] as $needle) {
|
||||
if (!str_contains($batchMethod, $needle)) {
|
||||
throw new RuntimeException("批量授权缺少安全边界 {$needle}");
|
||||
}
|
||||
}
|
||||
if (str_contains($logic, 'promotionPagePermissionAdminIdSet')
|
||||
|| str_contains($batchMethod, '尚未获得企业微信获客助手页面权限')
|
||||
|| str_contains($batchMethod, '$operatorAdminId === $ownerAdminId')) {
|
||||
throw new RuntimeException('批量授权仍存在预先页面权限或归属人跳过条件');
|
||||
}
|
||||
|
||||
$operatorAccess = $sources['operatorAccessPath'];
|
||||
foreach (['hasBasePagePermission', 'hasSharedPagePermission', 'hasPagePermission', 'visibleAdminIds', 'activePoolIds'] as $needle) {
|
||||
if (!str_contains($operatorAccess, $needle)) {
|
||||
throw new RuntimeException("共享页面权限服务缺少 {$needle}");
|
||||
}
|
||||
}
|
||||
if (!str_contains($operatorAccess, "join('qywx_promotion_pool p', 'p.id = po.pool_id')")
|
||||
|| !str_contains($operatorAccess, "whereNull('p.delete_time')")) {
|
||||
throw new RuntimeException('共享页面权限未排除已删除方案');
|
||||
}
|
||||
|
||||
if (!str_contains($sources['authLogicPath'], 'appendSharedPromotionPermission')
|
||||
|| !str_contains($sources['menuLogicPath'], 'sharedPromotionMenuIds')) {
|
||||
throw new RuntimeException('共享账号未动态获得页面权限或导航菜单');
|
||||
}
|
||||
if (!str_contains($sources['authMiddlewarePath'], "str_starts_with(\$accessUri, 'firstvisit.wecompromotion/')")
|
||||
|| !str_contains($sources['authMiddlewarePath'], 'isKnownWecomPromotionAction')) {
|
||||
throw new RuntimeException('获客助手子接口未统一纳入页面权限中间件');
|
||||
}
|
||||
|
||||
if (!str_contains($sources['customerLogicPath'], 'operatorPoolIds($adminId)')
|
||||
|| !str_contains($sources['customerLogicPath'], "whereOr('p.id', 'in', \$operatorPoolIds)")) {
|
||||
|| !str_contains($sources['customerLogicPath'], "whereOr('p.id', 'in', \$operatorPoolIds)")
|
||||
|| !str_contains($sources['customerLogicPath'], 'QywxPromotionOperatorAccess::visibleAdminIds')) {
|
||||
throw new RuntimeException('共享操作人尚未接入获客客户统计权限');
|
||||
}
|
||||
if (!str_contains($sources['controllerPath'], 'public function batchSetOperators()')) {
|
||||
@@ -57,5 +113,9 @@ foreach (['批量设置访问操作', '添加操作人', '移除操作人', 'sel
|
||||
throw new RuntimeException("前端共享权限交互缺少 {$needle}");
|
||||
}
|
||||
}
|
||||
if (str_contains($sources['viewPath'], '无页面权限')
|
||||
|| !str_contains($sources['viewPath'], '授权后账号会自动获得本页面入口')) {
|
||||
throw new RuntimeException('前端仍将预先拥有页面权限作为授权条件');
|
||||
}
|
||||
|
||||
echo "WECOM_PROMOTION_POOL_OPERATOR_CONTRACT_OK\n";
|
||||
|
||||
Reference in New Issue
Block a user