diff --git a/admin/src/api/asset.ts b/admin/src/api/asset.ts new file mode 100644 index 00000000..d54e1b0d --- /dev/null +++ b/admin/src/api/asset.ts @@ -0,0 +1,29 @@ +import request from '@/utils/request' + +// 账号管理 API +export function apiAssetUserList(params: any) { + return request.get({ url: '/asset.AssetUser/lists', params }) +} +export function apiAssetUserAdd(params: any) { + return request.post({ url: '/asset.AssetUser/add', params }) +} +export function apiAssetUserEdit(params: any) { + return request.post({ url: '/asset.AssetUser/edit', params }) +} +export function apiAssetUserDelete(params: any) { + return request.post({ url: '/asset.AssetUser/delete', params }) +} + +// 资源管理 API +export function apiAssetResourceList(params: any) { + return request.get({ url: '/asset.AssetResource/lists', params }) +} +export function apiAssetResourceAdd(params: any) { + return request.post({ url: '/asset.AssetResource/add', params }) +} +export function apiAssetResourceEdit(params: any) { + return request.post({ url: '/asset.AssetResource/edit', params }) +} +export function apiAssetResourceDelete(params: any) { + return request.post({ url: '/asset.AssetResource/delete', params }) +} diff --git a/admin/src/components/upload/index.vue b/admin/src/components/upload/index.vue index a51838c8..eead4783 100644 --- a/admin/src/components/upload/index.vue +++ b/admin/src/components/upload/index.vue @@ -102,8 +102,8 @@ export default defineComponent({ const visible = ref(false) const fileList = ref([]) - // 仅 video + direct 时才接管 http-request - const useDirect = computed(() => props.direct && props.type === 'video') + // 仅 video/voice + direct 时才接管 http-request + const useDirect = computed(() => props.direct && ['video', 'voice'].includes(props.type)) const handleProgress = () => { visible.value = true @@ -151,6 +151,8 @@ export default defineComponent({ return '.jpg,.png,.gif,.jpeg,.ico' case 'video': return '.wmv,.avi,.mpg,.mpeg,.3gp,.mov,.mp4,.flv,.rmvb,.mkv' + case 'voice': + return '.mp3,.wav,.wma,.m4a,.aac,.amr' default: return '*' } @@ -162,7 +164,7 @@ export default defineComponent({ try { const data = await uploadVideoDirectToCos({ file: options.file, - type: 'video', + type: props.type as any, cid: Number((options.data as any)?.cid ?? 0), onProgress(info) { // 触发 ElUpload 内部进度(保持与默认上传一致的体验) diff --git a/admin/src/views/asset/resource/index.vue b/admin/src/views/asset/resource/index.vue new file mode 100644 index 00000000..ad14974c --- /dev/null +++ b/admin/src/views/asset/resource/index.vue @@ -0,0 +1,291 @@ + + + diff --git a/admin/src/views/asset/user/index.vue b/admin/src/views/asset/user/index.vue new file mode 100644 index 00000000..17e20544 --- /dev/null +++ b/admin/src/views/asset/user/index.vue @@ -0,0 +1,204 @@ + + + diff --git a/server/app/adminapi/controller/asset/AssetResourceController.php b/server/app/adminapi/controller/asset/AssetResourceController.php new file mode 100644 index 00000000..11087f49 --- /dev/null +++ b/server/app/adminapi/controller/asset/AssetResourceController.php @@ -0,0 +1,162 @@ +request->get('page_no', 1); + $pageSize = $this->request->get('page_size', 15); + $type = $this->request->get('type'); + $title = $this->request->get('title', ''); + $startTime = $this->request->get('start_time'); + $endTime = $this->request->get('end_time'); + + $where = []; + if ($type) { + $where[] = ['type', '=', $type]; + } + if ($title) { + $where[] = ['title', 'like', '%' . $title . '%']; + } + if ($startTime) { + $where[] = ['create_time', '>=', strtotime($startTime)]; + } + if ($endTime) { + $where[] = ['create_time', '<=', strtotime($endTime) + 86399]; + } + + $count = AssetResource::where($where)->count(); + $lists = AssetResource::with('users') + ->where($where) + ->order('id', 'desc') + ->page($pageNo, $pageSize) + ->select(); + + return $this->data([ + 'count' => $count, + 'lists' => $lists, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + ]); + } + + /** + * @notes 添加资源及分配账号 + */ + public function add() + { + $params = $this->request->post(); + if (empty($params['type']) || empty($params['title']) || empty($params['file_url'])) { + return $this->fail('请填写完整的资源信息'); + } + + Db::startTrans(); + try { + $resource = AssetResource::create([ + 'type' => $params['type'], + 'title' => $params['title'], + 'file_url' => $params['file_url'], + 'cover_url' => $params['cover_url'] ?? '', + ]); + + // 绑定用户 + if (!empty($params['user_ids']) && is_array($params['user_ids'])) { + $userResources = []; + foreach ($params['user_ids'] as $userId) { + $userResources[] = [ + 'user_id' => $userId, + 'resource_id' => $resource->id, + 'create_time' => time(), + ]; + } + (new AssetUserResource())->saveAll($userResources); + } + + Db::commit(); + return $this->success('添加并分配成功', ['id' => $resource->id]); + } catch (\Exception $e) { + Db::rollback(); + return $this->fail('操作失败: ' . $e->getMessage()); + } + } + + /** + * @notes 编辑资源(修改标题、关联账号) + */ + public function edit() + { + $params = $this->request->post(); + $id = $params['id'] ?? 0; + if (empty($id)) { + return $this->fail('缺少参数'); + } + + $resource = AssetResource::find($id); + if (!$resource) { + return $this->fail('资源不存在'); + } + + Db::startTrans(); + try { + // 更新标题 + if (!empty($params['title'])) { + $resource->title = $params['title']; + $resource->save(); + } + + // 重新绑定用户(先删后加) + if (isset($params['user_ids'])) { + AssetUserResource::where('resource_id', $id)->delete(); + $userIds = is_array($params['user_ids']) ? $params['user_ids'] : []; + if (!empty($userIds)) { + $userResources = []; + foreach ($userIds as $userId) { + $userResources[] = [ + 'user_id' => $userId, + 'resource_id' => $id, + 'create_time' => time(), + ]; + } + (new AssetUserResource())->saveAll($userResources); + } + } + + Db::commit(); + return $this->success('编辑成功'); + } catch (\Exception $e) { + Db::rollback(); + return $this->fail('操作失败: ' . $e->getMessage()); + } + } + + /** + * @notes 删除资源 + */ + public function delete() + { + $id = $this->request->post('id'); + if (empty($id)) { + return $this->fail('缺少参数'); + } + + Db::startTrans(); + try { + AssetResource::destroy($id); + AssetUserResource::where('resource_id', $id)->delete(); + Db::commit(); + return $this->success('删除成功'); + } catch (\Exception $e) { + Db::rollback(); + return $this->fail('删除失败'); + } + } +} diff --git a/server/app/adminapi/controller/asset/AssetUserController.php b/server/app/adminapi/controller/asset/AssetUserController.php new file mode 100644 index 00000000..9095839c --- /dev/null +++ b/server/app/adminapi/controller/asset/AssetUserController.php @@ -0,0 +1,116 @@ +request->get('page_no', 1); + $pageSize = $this->request->get('page_size', 15); + $phone = $this->request->get('phone', ''); + + $where = []; + if ($phone) { + $where[] = ['phone', 'like', '%' . $phone . '%']; + } + + $count = AssetUser::where($where)->count(); + $lists = AssetUser::where($where) + ->order('id', 'desc') + ->page($pageNo, $pageSize) + ->select(); + + return $this->data([ + 'count' => $count, + 'lists' => $lists, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + ]); + } + + /** + * @notes 添加账号 + */ + public function add() + { + $params = $this->request->post(); + if (empty($params['phone'])) { + return $this->fail('手机号不能为空'); + } + + $exist = AssetUser::where('phone', $params['phone'])->find(); + if ($exist) { + return $this->fail('手机号已存在'); + } + + // Default password 123456 + $password = empty($params['password']) ? '123456' : $params['password']; + $passwordHash = password_hash($password, PASSWORD_DEFAULT); + + $user = AssetUser::create([ + 'phone' => $params['phone'], + 'password' => $passwordHash, + 'status' => $params['status'] ?? 1, + ]); + + return $this->success('添加成功', ['id' => $user->id]); + } + + /** + * @notes 编辑账号 + */ + public function edit() + { + $params = $this->request->post(); + if (empty($params['id'])) { + return $this->fail('缺少参数'); + } + + $user = AssetUser::find($params['id']); + if (!$user) { + return $this->fail('账号不存在'); + } + + if (!empty($params['phone']) && $params['phone'] != $user->phone) { + $exist = AssetUser::where('phone', $params['phone'])->find(); + if ($exist) { + return $this->fail('手机号已存在'); + } + $user->phone = $params['phone']; + } + + if (!empty($params['password'])) { + $user->password = password_hash($params['password'], PASSWORD_DEFAULT); + } + + if (isset($params['status'])) { + $user->status = $params['status']; + } + + $user->save(); + return $this->success('修改成功'); + } + + /** + * @notes 删除账号 + */ + public function delete() + { + $id = $this->request->post('id'); + if (empty($id)) { + return $this->fail('缺少参数'); + } + + AssetUser::destroy($id); + // 也需要删除关联的资源记录 + \app\common\model\AssetUserResource::where('user_id', $id)->delete(); + + return $this->success('删除成功'); + } +} diff --git a/server/app/api/controller/asset/AssetAppController.php b/server/app/api/controller/asset/AssetAppController.php new file mode 100644 index 00000000..6dbcd8ea --- /dev/null +++ b/server/app/api/controller/asset/AssetAppController.php @@ -0,0 +1,286 @@ +request->header('token'); + if (empty($token)) { + return null; + } + + // 方式1: 从数据库 token 字段查找 + try { + $user = AssetUser::where('token', $token) + ->where('token_expire_time', '>', time()) + ->find(); + if ($user) { + return $user; + } + } catch (\Throwable $e) { + // token 字段不存在时忽略,走 cache 兜底 + } + + // 方式2: 从 file cache 查找 + $userId = cache('asset_token_' . $token); + if ($userId) { + $user = AssetUser::where('id', $userId)->find(); + return $user ?: null; + } + + return null; + } + + /** + * @notes 获取当前有效用户(status=1 才返回) + */ + private function getAssetUser(): ?AssetUser + { + $user = $this->getAssetUserRaw(); + if ($user && $user->status == 1) { + return $user; + } + return null; + } + + /** + * @notes 小程序端登录 + */ + public function login() + { + $phone = $this->request->post('phone'); + $password = $this->request->post('password'); + + if (empty($phone) || empty($password)) { + return $this->fail('手机号或密码不能为空'); + } + + $user = AssetUser::where('phone', $phone)->find(); + if (!$user) { + return $this->fail('账号不存在'); + } + + if ($user->status != 1) { + return $this->fail('账号已被禁用'); + } + + if (!password_verify($password, $user->password)) { + return $this->fail('密码错误'); + } + + // 生成 token 并双写(DB + cache 兜底) + $token = md5($user->id . time() . uniqid('asset', true)); + + // 写入 file cache(始终可用) + cache('asset_token_' . $token, $user->id, self::TOKEN_EXPIRE); + + // 尝试写入数据库(需要已执行 SQL 迁移) + try { + $user->token = $token; + $user->token_expire_time = time() + self::TOKEN_EXPIRE; + $user->save(); + } catch (\Throwable $e) { + // token 字段不存在时忽略,cache 已经写入 + } + + return $this->success('登录成功', [ + 'token' => $token, + 'user' => [ + 'id' => $user->id, + 'phone' => $user->phone + ] + ]); + } + + /** + * @notes 获取用户关联的资源列表 (或公开资源) + */ + public function getResourceList() + { + $token = $this->request->header('token'); + $rawUser = !empty($token) ? $this->getAssetUserRaw() : null; + $user = ($rawUser && $rawUser->status == 1) ? $rawUser : null; + $isDisabled = ($rawUser && $rawUser->status != 1); // 用户存在但被禁用 + $tokenInvalid = (!empty($token) && !$rawUser); // token 过期或无效 + + $type = $this->request->get('type', 1); + $pageNo = $this->request->get('page_no', 1); + $pageSize = $this->request->get('page_size', 20); + $days = (int)$this->request->get('days', 0); + $usageStatus = (int)$this->request->get('usage_status', 0); // 0=全部, 1=未使用, 2=已使用 + + $query = AssetResource::where('type', $type); + $usedResourceIds = []; + + if ($user) { + // 登录用户:自己的专属资源 + 所有公开资源 + $exclusiveIds = AssetUserResource::where('user_id', $user->id)->column('resource_id'); + $associatedResourceIds = AssetUserResource::column('resource_id'); + + $query = $query->where(function ($q) use ($exclusiveIds, $associatedResourceIds) { + if (!empty($exclusiveIds)) { + $q->whereIn('id', $exclusiveIds); + } + if (!empty($associatedResourceIds)) { + if (!empty($exclusiveIds)) { + $q->whereOr('id', 'not in', $associatedResourceIds); + } else { + $q->whereNotIn('id', $associatedResourceIds); + } + } + }); + + // 获取该用户的使用记录 + $usedResourceIds = \think\facade\Db::name('asset_resource_usage') + ->where('user_id', $user->id) + ->column('resource_id'); + + // 使用状态过滤 + if ($usageStatus === 1) { // 未使用 + if (!empty($usedResourceIds)) { + $query = $query->whereNotIn('id', $usedResourceIds); + } + } elseif ($usageStatus === 2) { // 已使用 + if (empty($usedResourceIds)) { + return $this->data([ + 'lists' => [], + 'count' => 0, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + 'is_disabled' => $isDisabled, + 'token_invalid' => $tokenInvalid, + ]); + } + $query = $query->whereIn('id', $usedResourceIds); + } + } else { + // 游客(未登录):只看公开资源(没关联给任何用户的资源) + $associatedResourceIds = AssetUserResource::column('resource_id'); + if (!empty($associatedResourceIds)) { + $query = $query->whereNotIn('id', $associatedResourceIds); + } + } + + // 时间过滤 + if ($days > 0) { + $startTime = strtotime("-{$days} days", strtotime(date('Y-m-d'))); + $query = $query->where('create_time', '>=', $startTime); + } + + $count = (clone $query)->count(); + $lists = (clone $query) + ->order('id', 'desc') + ->page($pageNo, $pageSize) + ->select() + ->toArray(); + + // 附加 is_used 字段 + foreach ($lists as &$item) { + $item['is_used'] = in_array($item['id'], $usedResourceIds); + } + + return $this->data([ + 'lists' => $lists, + 'count' => $count, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + 'is_disabled' => $isDisabled, // 用户被禁用 + 'token_invalid' => $tokenInvalid, // token 过期或无效 + ]); + } + + /** + * @notes 记录资源下载/使用 + */ + public function recordDownload() + { + $user = $this->getAssetUser(); + if (!$user) { + return $this->fail('请先登录', [], -1); + } + + $resourceId = $this->request->post('resource_id'); + if (empty($resourceId)) { + return $this->fail('参数缺失'); + } + + // 检查资源是否存在 + $resource = AssetResource::find($resourceId); + if (!$resource) { + return $this->fail('资源不存在'); + } + + // 检查是否有关联权限 (或者是公开资源) + $isAssociated = AssetUserResource::where('user_id', $user->id)->where('resource_id', $resourceId)->find(); + $isPublic = !AssetUserResource::where('resource_id', $resourceId)->find(); + + if (!$isAssociated && !$isPublic) { + return $this->fail('无权操作此资源'); + } + + // 记录下载 + $exists = \think\facade\Db::name('asset_resource_usage') + ->where('user_id', $user->id) + ->where('resource_id', $resourceId) + ->find(); + + if (!$exists) { + \think\facade\Db::name('asset_resource_usage')->insert([ + 'user_id' => $user->id, + 'resource_id' => $resourceId, + 'create_time' => time() + ]); + } + + return $this->success('记录成功'); + } + + /** + * @notes 修改密码 + */ + public function changePassword() + { + $user = $this->getAssetUser(); + if (!$user) { + return $this->fail('登录已过期,请重新登录', [], -1); + } + + $oldPassword = $this->request->post('old_password'); + $password = $this->request->post('password'); + + if (empty($oldPassword)) { + return $this->fail('请输入原密码'); + } + if (empty($password)) { + return $this->fail('请输入新密码'); + } + if (strlen($password) < 6) { + return $this->fail('新密码至少6位'); + } + + if (!password_verify($oldPassword, $user->password)) { + return $this->fail('原密码不正确'); + } + + $user->password = password_hash($password, PASSWORD_DEFAULT); + $user->save(); + + return $this->success('密码修改成功'); + } +} diff --git a/server/app/common/model/AssetResource.php b/server/app/common/model/AssetResource.php new file mode 100644 index 00000000..302f5642 --- /dev/null +++ b/server/app/common/model/AssetResource.php @@ -0,0 +1,36 @@ +belongsToMany(AssetUser::class, AssetUserResource::class, 'user_id', 'resource_id'); + } +} diff --git a/server/app/common/model/AssetUser.php b/server/app/common/model/AssetUser.php new file mode 100644 index 00000000..10cd222c --- /dev/null +++ b/server/app/common/model/AssetUser.php @@ -0,0 +1,13 @@ + 2 * 1024 * 1024 * 1024, // 2GB + self::TYPE_VOICE => 500 * 1024 * 1024, // 500MB ]; /** @@ -144,6 +146,7 @@ class DirectUploadService { return match ($type) { self::TYPE_VIDEO => FileEnum::VIDEO_TYPE, + self::TYPE_VOICE => FileEnum::FILE_TYPE, default => FileEnum::FILE_TYPE, }; } diff --git a/server/sql/asset_menu.sql b/server/sql/asset_menu.sql new file mode 100644 index 00000000..bd9e7ee8 --- /dev/null +++ b/server/sql/asset_menu.sql @@ -0,0 +1,38 @@ +-- 专属数字资产 菜单和权限配置 +-- 注意:需要根据实际的菜单表结构调整字段名称(如前缀 zyt_ 可能需根据您的实际表前缀替换) + +-- 1. 添加一级菜单:资源分发管理 +INSERT INTO `zyt_system_menu` (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) +VALUES (0, 'M', '资源分发管理', 'el-icon-FolderOpened', 110, '', '/asset', '', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + +-- 获取刚插入的一级菜单ID +SET @asset_pid = LAST_INSERT_ID(); + +-- 2. 添加二级菜单:分发账号管理 +INSERT INTO `zyt_system_menu` (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) +VALUES (@asset_pid, 'C', '分发账号管理', 'el-icon-User', 1, 'asset.AssetUser/lists', '/asset/user', '/asset/user/index', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + +-- 获取【分发账号管理】菜单ID +SET @user_menu_id = LAST_INSERT_ID(); + +-- 3. 添加账号相关按钮权限 +INSERT INTO `zyt_system_menu` (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) +VALUES +(@user_menu_id, 'A', '新增账号', '', 1, 'asset.AssetUser/add', '', '', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(@user_menu_id, 'A', '编辑账号', '', 2, 'asset.AssetUser/edit', '', '', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(@user_menu_id, 'A', '删除账号', '', 3, 'asset.AssetUser/delete', '', '', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + + +-- 4. 添加二级菜单:资源素材下发 +INSERT INTO `zyt_system_menu` (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) +VALUES (@asset_pid, 'C', '资源素材下发', 'el-icon-Files', 2, 'asset.AssetResource/lists', '/asset/resource', '/asset/resource/index', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + +-- 获取【资源素材下发】菜单ID +SET @resource_menu_id = LAST_INSERT_ID(); + +-- 5. 添加资源相关按钮权限 +INSERT INTO `zyt_system_menu` (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) +VALUES +(@resource_menu_id, 'A', '上传分配资源', '', 1, 'asset.AssetResource/add', '', '', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +(@resource_menu_id, 'A', '删除资源', '', 2, 'asset.AssetResource/delete', '', '', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + diff --git a/server/sql/asset_resource_usage.sql b/server/sql/asset_resource_usage.sql new file mode 100644 index 00000000..4feaa9f3 --- /dev/null +++ b/server/sql/asset_resource_usage.sql @@ -0,0 +1,8 @@ +CREATE TABLE `zyt_asset_resource_usage` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(11) unsigned NOT NULL COMMENT '用户ID', + `resource_id` int(11) unsigned NOT NULL COMMENT '资源ID', + `create_time` int(11) DEFAULT '0' COMMENT '使用/下载时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_resource` (`user_id`, `resource_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私密资产-资源使用记录表'; diff --git a/server/sql/asset_system.sql b/server/sql/asset_system.sql new file mode 100644 index 00000000..8f76dc62 --- /dev/null +++ b/server/sql/asset_system.sql @@ -0,0 +1,31 @@ +CREATE TABLE `zyt_asset_user` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `phone` varchar(20) NOT NULL COMMENT '手机号(登录账号)', + `password` varchar(255) NOT NULL COMMENT '密码', + `status` tinyint(1) DEFAULT '1' COMMENT '状态:1=正常,0=禁用', + `create_time` int(11) DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) DEFAULT '0' COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_phone` (`phone`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私密资产-用户表'; + +CREATE TABLE `zyt_asset_resource` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '资源类型:1=图片,2=视频,3=语音', + `title` varchar(100) NOT NULL COMMENT '资源名称/标题', + `file_url` varchar(500) NOT NULL COMMENT '文件链接', + `cover_url` varchar(500) DEFAULT '' COMMENT '封面图链接(针对视频/语音)', + `create_time` int(11) DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) DEFAULT '0' COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私密资产-资源表'; + +CREATE TABLE `zyt_asset_user_resource` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(11) unsigned NOT NULL COMMENT '用户ID', + `resource_id` int(11) unsigned NOT NULL COMMENT '资源ID', + `create_time` int(11) DEFAULT '0' COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_resource_id` (`resource_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私密资产-用户与资源关联表'; diff --git a/server/sql/asset_user_token.sql b/server/sql/asset_user_token.sql new file mode 100644 index 00000000..30ea3f29 --- /dev/null +++ b/server/sql/asset_user_token.sql @@ -0,0 +1,4 @@ +-- 为 asset_user 表添加 token 持久化字段(替代 file cache,避免 GC 清理导致 token 丢失) +ALTER TABLE `zyt_asset_user` ADD COLUMN `token` varchar(64) DEFAULT '' COMMENT '登录token' AFTER `status`; +ALTER TABLE `zyt_asset_user` ADD COLUMN `token_expire_time` int(11) DEFAULT '0' COMMENT 'token过期时间戳' AFTER `token`; +ALTER TABLE `zyt_asset_user` ADD INDEX `idx_token` (`token`);