feat: 项目负责人+内部任务群+portal 内部身份领取+群主链接归属

- 迁移 0012:projects.owner_userid / tasks.task_chat_id / claims.owner_partner_id
- lib/task-group-service.ts:懒建 appchat 任务群(首个内部群主领取时创建,成员 diff 补员),播报/卡片文案
- wecom-client 新增 appchat create/update/get/send 封装
- OAuth ticket 增可选 uid(v:1 兼容),内部成员领取按 wecom_userid 解析群主身份
- 外部 OAuth 领取人 upsert 打 partner_type='group_owner'
- portal 链接带 ?owner= 穿透 OAuth 往返,claims 记录归属群主
- 后台任务卡「群主链接」面板:复制链接/推送内部群主/任务群状态
- 催办与超期升级同步播报进任务群
This commit is contained in:
ABAPPLO
2026-09-03 11:30:26 +08:00
parent 5d2a17f5b5
commit bd80b2d7d8
17 changed files with 1466 additions and 39 deletions

View File

@@ -5,6 +5,7 @@ export type ProjectRow = {
name: string;
brand: string;
main_group_member_ids: string;
owner_userid: string;
status: string;
note: string;
created_at: string;
@@ -18,6 +19,7 @@ export type ProjectSummary = {
note: string;
createdAt: string;
mainGroupMemberIds: string[];
ownerUserid: string;
taskCount: number;
groupChatCount: number;
};
@@ -95,7 +97,7 @@ function parseJsonArray(value: string): string[] {
export async function listProjects(db: DatabaseClient): Promise<ProjectSummary[]> {
const result = await db
.prepare(
`SELECT p.id, p.name, p.brand, p.main_group_member_ids, p.status, p.note, p.created_at,
`SELECT p.id, p.name, p.brand, p.main_group_member_ids, p.owner_userid, p.status, p.note, p.created_at,
(SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id) AS task_count,
(SELECT COUNT(*) FROM project_group_chats g WHERE g.project_id = p.id) AS group_chat_count
FROM projects p
@@ -110,6 +112,7 @@ export async function listProjects(db: DatabaseClient): Promise<ProjectSummary[]
note: row.note,
createdAt: row.created_at,
mainGroupMemberIds: parseJsonArray(row.main_group_member_ids),
ownerUserid: row.owner_userid ?? "",
taskCount: Number(row.task_count),
groupChatCount: Number(row.group_chat_count),
}));
@@ -122,7 +125,7 @@ export async function getProjectDetail(
const id = text(projectId);
const project = await db
.prepare(
`SELECT id, name, brand, main_group_member_ids, status, note, created_at
`SELECT id, name, brand, main_group_member_ids, owner_userid, status, note, created_at
FROM projects WHERE id = ?`,
)
.bind(id)
@@ -170,6 +173,7 @@ export async function getProjectDetail(
note: project.note,
createdAt: project.created_at,
mainGroupMemberIds: parseJsonArray(project.main_group_member_ids),
ownerUserid: project.owner_userid ?? "",
taskCount: tasks.results.length,
groupChatCount: chats.results.length,
},
@@ -192,6 +196,7 @@ export type SaveProjectInput = {
name?: unknown;
brand?: unknown;
mainGroupMemberIds?: unknown;
ownerUserid?: unknown;
note?: unknown;
};
@@ -204,6 +209,7 @@ export async function saveProject(
const brand = text(input.brand);
const note = text(input.note ?? "");
const memberIds = parseMemberIds(input.mainGroupMemberIds);
const ownerUserid = text(input.ownerUserid).slice(0, 64);
if (!name) throw new ProjectError("请填写项目名称");
const now = new Date().toISOString();
@@ -212,10 +218,10 @@ export async function saveProject(
const result = await db
.prepare(
`UPDATE projects
SET name = ?, brand = ?, main_group_member_ids = ?, note = ?
SET name = ?, brand = ?, main_group_member_ids = ?, owner_userid = ?, note = ?
WHERE id = ?`,
)
.bind(name, brand, JSON.stringify(memberIds), note, id)
.bind(name, brand, JSON.stringify(memberIds), ownerUserid, note, id)
.run();
if (result.meta.changes === 0) throw new ProjectError("项目不存在", 404);
} else {
@@ -223,14 +229,15 @@ export async function saveProject(
await db
.prepare(
`INSERT INTO projects
(id, name, brand, main_group_member_ids, status, note, created_at)
VALUES (?, ?, ?, ?, 'active', ?, ?)`,
(id, name, brand, main_group_member_ids, owner_userid, status, note, created_at)
VALUES (?, ?, ?, ?, ?, 'active', ?, ?)`,
)
.bind(
projectId,
name,
brand,
JSON.stringify(memberIds),
ownerUserid,
note,
now,
)