## GitHub 同步说明
| 项 | 内容 |
|:---|:---|
| 仓库 | https://github.com/fnvtk/cunkebao_v3 |
| 分支 | develop |
| 方向 | 本地 → GitHub(单向 push) |
| 是否拉取远程 | 否(未 fetch / pull / merge) |
| 是否改本地文件 | 否(仅 git add/commit,未编辑任何源文件正文) |
| 基准 commit | 0643e78a |
| 变更规模 | 709 files, +26436 / -3407 lines |
## 目录变更统计
| 目录 | 文件数 | 说明 |
|:---|---:|:---|
| 开发文档/ | 404 | 四端进行中/已完成需求、接口文档、官网需求、部署脚本 |
| Server/ | 95 | 后端 API、迁移脚本、触客宝/超管/存客宝服务 |
| Cunkebao/ | 85 | 移动端 H5、设备绑定、客服、转号审批等 |
| Touchkebao/ | 68 | PC 工作台、AI 获客、算力中心、Glass UI |
| SuperAdmin/ | 33 | 超管项目中心、算力计费、内容库 |
| 官网/ | 19 | 新增存客宝官网静态页(index/pricing/solutions 等) |
| .cursor/ | 3 | Cursor 规则与 Skill |
| .obsidian/ | 2 | Obsidian 工作区配置 |
## 重点新增/更新(本地为准)
- 官网/: 完整静态站点(index、pricing、manual、solutions、api)
- 开发文档/1、需求/修改/*_进行中_20260530.md(存客宝/触客宝/AI数智员工)
- 开发文档/1、需求/官网需求.md
- 开发文档/5、接口/开发进度_接口文档.md 等接口文档迭代
- Touchkebao Glass UI 与 P0 收尾、通道获客算力
- SuperAdmin 算力计费规则、项目工作台
## 刻意未上传(本地保留)
- node_modules/
- .DS_Store、.smart-env/
- .开发文档_nested_git_backup/(旧嵌套 git 备份)
- **/__pycache__/
## 验收
GitHub 浏览: https://github.com/fnvtk/cunkebao_v3/tree/develop
Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
6.8 KiB
PHP
181 lines
6.8 KiB
PHP
<?php
|
||
|
||
namespace app\superadmin\service;
|
||
|
||
use think\Db;
|
||
|
||
/**
|
||
* 内容库多项目分配(§6.4)
|
||
*
|
||
* - 表 ck_content_library_assign:libraryId × companyId 多对多,每项目独立 enabled
|
||
* - content_library.companyId 保留为主归属;分配 = 共享,不复制素材
|
||
* - 存客宝读库优先 assign 表 enabled=1,回落 companyId(读取方各自实现)
|
||
*/
|
||
class ContentLibraryAssignService
|
||
{
|
||
/** 各库已分配项目数 map:libraryId => count(enabled 不限) */
|
||
public function assignedCountMap(array $libraryIds): array
|
||
{
|
||
$libraryIds = array_values(array_unique(array_filter(array_map('intval', $libraryIds))));
|
||
if (empty($libraryIds)) {
|
||
return [];
|
||
}
|
||
$map = [];
|
||
try {
|
||
$rows = Db::name('content_library_assign')
|
||
->whereIn('libraryId', $libraryIds)
|
||
->field('libraryId, COUNT(*) as cnt')
|
||
->group('libraryId')
|
||
->select();
|
||
foreach ($rows ?: [] as $r) {
|
||
$map[(int) $r['libraryId']] = (int) $r['cnt'];
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 迁移未执行时返回空
|
||
}
|
||
return $map;
|
||
}
|
||
|
||
/** 单库的分配明细(含项目名 + 各项目 enabled) */
|
||
public function assignments(int $libraryId): array
|
||
{
|
||
$rows = Db::name('content_library_assign')->alias('a')
|
||
->leftJoin('company c', 'c.companyId = a.companyId')
|
||
->where('a.libraryId', $libraryId)
|
||
->field('a.id, a.libraryId, a.companyId, a.enabled, a.sort, c.name as projectName, c.id as projectId')
|
||
->order('a.sort', 'asc')->order('a.id', 'asc')
|
||
->select();
|
||
$list = [];
|
||
foreach ($rows ?: [] as $r) {
|
||
$list[] = [
|
||
'id' => (int) $r['id'],
|
||
'libraryId' => (int) $r['libraryId'],
|
||
'companyId' => (int) $r['companyId'],
|
||
'projectId' => (int) ($r['projectId'] ?? 0),
|
||
'projectName' => $r['projectName'] ?: ('项目#' . (int) $r['companyId']),
|
||
'enabled' => (int) $r['enabled'],
|
||
];
|
||
}
|
||
return $list;
|
||
}
|
||
|
||
/**
|
||
* 批量分配:libraryIds × targets[{companyId, enabled}]
|
||
* upsert(唯一键 libraryId+companyId)
|
||
*/
|
||
public function assign(array $libraryIds, array $targets): array
|
||
{
|
||
$libraryIds = array_values(array_unique(array_filter(array_map('intval', $libraryIds))));
|
||
if (empty($libraryIds)) {
|
||
throw new \Exception('缺少 libraryIds', 400);
|
||
}
|
||
if (empty($targets)) {
|
||
throw new \Exception('缺少目标项目', 400);
|
||
}
|
||
$now = time();
|
||
$affected = 0;
|
||
foreach ($libraryIds as $libId) {
|
||
foreach ($targets as $t) {
|
||
$companyId = (int) ($t['companyId'] ?? 0);
|
||
if ($companyId <= 0) {
|
||
continue;
|
||
}
|
||
$enabled = (int) (($t['enabled'] ?? 1) ? 1 : 0);
|
||
$exists = Db::name('content_library_assign')
|
||
->where('libraryId', $libId)->where('companyId', $companyId)->find();
|
||
if ($exists) {
|
||
Db::name('content_library_assign')->where('id', $exists['id'])
|
||
->update(['enabled' => $enabled, 'updateTime' => $now]);
|
||
} else {
|
||
Db::name('content_library_assign')->insert([
|
||
'libraryId' => $libId,
|
||
'companyId' => $companyId,
|
||
'enabled' => $enabled,
|
||
'sort' => 0,
|
||
'createTime' => $now,
|
||
'updateTime' => $now,
|
||
]);
|
||
}
|
||
$affected++;
|
||
}
|
||
}
|
||
return ['affected' => $affected, 'libraryIds' => $libraryIds, 'targetCount' => count($targets)];
|
||
}
|
||
|
||
/** 单条分配启停 */
|
||
public function toggle(int $assignId, int $enabled): array
|
||
{
|
||
$row = Db::name('content_library_assign')->where('id', $assignId)->find();
|
||
if (!$row) {
|
||
throw new \Exception('分配记录不存在', 404);
|
||
}
|
||
Db::name('content_library_assign')->where('id', $assignId)
|
||
->update(['enabled' => $enabled ? 1 : 0, 'updateTime' => time()]);
|
||
return ['id' => $assignId, 'enabled' => $enabled ? 1 : 0];
|
||
}
|
||
|
||
/** 从 JSON / 逗号分隔的 url 字段取第一个 */
|
||
protected function firstUrl($raw): string
|
||
{
|
||
$raw = (string) $raw;
|
||
if ($raw === '') {
|
||
return '';
|
||
}
|
||
$decoded = json_decode($raw, true);
|
||
if (is_array($decoded)) {
|
||
foreach ($decoded as $v) {
|
||
if (is_string($v) && $v !== '') {
|
||
return $v;
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
$parts = preg_split('/[,\s]+/', $raw);
|
||
return $parts[0] ?? '';
|
||
}
|
||
|
||
/** 库内素材预览(分页) */
|
||
public function materials(int $libraryId, int $page = 1, int $limit = 20): array
|
||
{
|
||
$page = max(1, $page);
|
||
$limit = min(100, max(1, $limit));
|
||
$lib = Db::name('content_library')->where('id', $libraryId)->where('isDel', 0)->find();
|
||
if (!$lib) {
|
||
throw new \Exception('内容库不存在', 404);
|
||
}
|
||
$base = Db::name('content_item')->where('libraryId', $libraryId)->where('isDel', 0);
|
||
$total = (clone $base)->count();
|
||
$rows = $base->order('id', 'desc')
|
||
->page($page, $limit)
|
||
->select();
|
||
$items = [];
|
||
foreach ($rows ?: [] as $r) {
|
||
$title = trim((string) ($r['title'] ?? ''));
|
||
if ($title === '') {
|
||
$title = mb_substr(strip_tags((string) ($r['content'] ?? '')), 0, 30);
|
||
}
|
||
$thumb = (string) ($r['coverImage'] ?? '');
|
||
if ($thumb === '') {
|
||
$thumb = $this->firstUrl($r['ossUrls'] ?? '') ?: $this->firstUrl($r['urls'] ?? '') ?: $this->firstUrl($r['resUrls'] ?? '');
|
||
}
|
||
$items[] = [
|
||
'id' => (int) $r['id'],
|
||
'title' => $title !== '' ? $title : ('素材#' . (int) $r['id']),
|
||
'type' => $r['type'] ?? ($r['contentType'] ?? ''),
|
||
'thumb' => $thumb,
|
||
'updateTime' => !empty($r['updateTime'])
|
||
? date('Y-m-d H:i:s', (int) $r['updateTime'])
|
||
: (!empty($r['createTime']) ? date('Y-m-d H:i:s', (int) $r['createTime']) : ''),
|
||
];
|
||
}
|
||
return [
|
||
'libraryId' => $libraryId,
|
||
'libraryName' => $lib['name'] ?? '',
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'items' => $items,
|
||
];
|
||
}
|
||
}
|