From 485ba7a3801092b38289f6135e7e362a582fd5ae Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 20 May 2026 16:06:46 +0800 Subject: [PATCH 01/18] style: optimize ui for login and index pages with modern ios tech style --- admin/src/api/asset.ts | 26 +++ admin/src/views/asset/resource/index.vue | 178 ++++++++++++++++++ admin/src/views/asset/user/index.vue | 151 +++++++++++++++ .../asset/AssetResourceController.php | 101 ++++++++++ .../controller/asset/AssetUserController.php | 109 +++++++++++ .../controller/asset/AssetAppController.php | 98 ++++++++++ server/app/common/model/AssetResource.php | 36 ++++ server/app/common/model/AssetUser.php | 13 ++ server/app/common/model/AssetUserResource.php | 9 + server/sql/asset_system.sql | 31 +++ 10 files changed, 752 insertions(+) create mode 100644 admin/src/api/asset.ts create mode 100644 admin/src/views/asset/resource/index.vue create mode 100644 admin/src/views/asset/user/index.vue create mode 100644 server/app/adminapi/controller/asset/AssetResourceController.php create mode 100644 server/app/adminapi/controller/asset/AssetUserController.php create mode 100644 server/app/api/controller/asset/AssetAppController.php create mode 100644 server/app/common/model/AssetResource.php create mode 100644 server/app/common/model/AssetUser.php create mode 100644 server/app/common/model/AssetUserResource.php create mode 100644 server/sql/asset_system.sql diff --git a/admin/src/api/asset.ts b/admin/src/api/asset.ts new file mode 100644 index 00000000..7da0b015 --- /dev/null +++ b/admin/src/api/asset.ts @@ -0,0 +1,26 @@ +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 apiAssetResourceDelete(params: any) { + return request.post({ url: '/asset/AssetResource/delete', params }) +} diff --git a/admin/src/views/asset/resource/index.vue b/admin/src/views/asset/resource/index.vue new file mode 100644 index 00000000..9b71d63e --- /dev/null +++ b/admin/src/views/asset/resource/index.vue @@ -0,0 +1,178 @@ + + + diff --git a/admin/src/views/asset/user/index.vue b/admin/src/views/asset/user/index.vue new file mode 100644 index 00000000..28ea76cf --- /dev/null +++ b/admin/src/views/asset/user/index.vue @@ -0,0 +1,151 @@ + + + diff --git a/server/app/adminapi/controller/asset/AssetResourceController.php b/server/app/adminapi/controller/asset/AssetResourceController.php new file mode 100644 index 00000000..9de71819 --- /dev/null +++ b/server/app/adminapi/controller/asset/AssetResourceController.php @@ -0,0 +1,101 @@ +request->get('page_no', 1); + $pageSize = $this->request->get('page_size', 15); + $type = $this->request->get('type'); + + $where = []; + if ($type) { + $where[] = ['type', '=', $type]; + } + + $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 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..8d25dc1e --- /dev/null +++ b/server/app/adminapi/controller/asset/AssetUserController.php @@ -0,0 +1,109 @@ +request->get('page_no', 1); + $pageSize = $this->request->get('page_size', 15); + + $count = AssetUser::count(); + $lists = AssetUser::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..78f2ce42 --- /dev/null +++ b/server/app/api/controller/asset/AssetAppController.php @@ -0,0 +1,98 @@ +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 (实际项目中建议用 JWT 或 Redis 存储 token) + $token = md5($user->id . time() . 'secret'); + cache('asset_token_' . $token, $user->id, 86400 * 7); + + return $this->success('登录成功', [ + 'token' => $token, + 'user' => [ + 'id' => $user->id, + 'phone' => $user->phone + ] + ]); + } + + /** + * @notes 获取用户关联的资源列表 + */ + public function getResourceList() + { + $token = $this->request->header('token'); + if (empty($token)) { + return $this->fail('请先登录', [], 401); + } + + $userId = cache('asset_token_' . $token); + if (!$userId) { + return $this->fail('登录已过期', [], 401); + } + + $type = $this->request->get('type', 1); // 1:图片 2:视频 3:语音 + $pageNo = $this->request->get('page_no', 1); + $pageSize = $this->request->get('page_size', 20); + + // 获取用户关联的资源 ID 列表 + $resourceIds = AssetUserResource::where('user_id', $userId)->column('resource_id'); + + if (empty($resourceIds)) { + return $this->data([ + 'lists' => [], + 'count' => 0, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + ]); + } + + $count = AssetResource::whereIn('id', $resourceIds)->where('type', $type)->count(); + $lists = AssetResource::whereIn('id', $resourceIds) + ->where('type', $type) + ->order('id', 'desc') + ->page($pageNo, $pageSize) + ->select(); + + return $this->data([ + 'lists' => $lists, + 'count' => $count, + 'page_no' => $pageNo, + 'page_size' => $pageSize, + ]); + } +} 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 @@ + Date: Wed, 20 May 2026 16:39:22 +0800 Subject: [PATCH 02/18] feat: use material-picker for resource cos direct upload --- admin/src/views/asset/resource/index.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/admin/src/views/asset/resource/index.vue b/admin/src/views/asset/resource/index.vue index 9b71d63e..992fac9e 100644 --- a/admin/src/views/asset/resource/index.vue +++ b/admin/src/views/asset/resource/index.vue @@ -58,8 +58,8 @@ - - + + @@ -79,6 +79,7 @@ import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { apiAssetResourceList, apiAssetResourceAdd, apiAssetResourceDelete, apiAssetUserList } from '@/api/asset' +import MaterialPicker from '@/components/material/picker.vue' const loading = ref(false) const tableData = ref([]) From 61d14d1b45d25b66574dedfe3bd92146518b682f Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 20 May 2026 16:43:57 +0800 Subject: [PATCH 03/18] fix: Pivot model exception --- server/app/common/model/AssetUserResource.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/app/common/model/AssetUserResource.php b/server/app/common/model/AssetUserResource.php index 145bb281..60661a77 100644 --- a/server/app/common/model/AssetUserResource.php +++ b/server/app/common/model/AssetUserResource.php @@ -1,7 +1,8 @@ Date: Wed, 20 May 2026 16:53:08 +0800 Subject: [PATCH 04/18] feat: add dedicated change password api and fix token expire code --- admin/src/api/asset.ts | 14 +++---- .../controller/asset/AssetAppController.php | 37 +++++++++++++++++- server/sql/asset_menu.sql | 38 +++++++++++++++++++ server/sql/asset_system.sql | 6 +-- 4 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 server/sql/asset_menu.sql diff --git a/admin/src/api/asset.ts b/admin/src/api/asset.ts index 7da0b015..46dc3317 100644 --- a/admin/src/api/asset.ts +++ b/admin/src/api/asset.ts @@ -2,25 +2,25 @@ import request from '@/utils/request' // 账号管理 API export function apiAssetUserList(params: any) { - return request.get({ url: '/asset/AssetUser/lists', params }) + return request.get({ url: '/asset.AssetUser/lists', params }) } export function apiAssetUserAdd(params: any) { - return request.post({ url: '/asset/AssetUser/add', params }) + return request.post({ url: '/asset.AssetUser/add', params }) } export function apiAssetUserEdit(params: any) { - return request.post({ url: '/asset/AssetUser/edit', params }) + return request.post({ url: '/asset.AssetUser/edit', params }) } export function apiAssetUserDelete(params: any) { - return request.post({ url: '/asset/AssetUser/delete', params }) + return request.post({ url: '/asset.AssetUser/delete', params }) } // 资源管理 API export function apiAssetResourceList(params: any) { - return request.get({ url: '/asset/AssetResource/lists', params }) + return request.get({ url: '/asset.AssetResource/lists', params }) } export function apiAssetResourceAdd(params: any) { - return request.post({ url: '/asset/AssetResource/add', params }) + return request.post({ url: '/asset.AssetResource/add', params }) } export function apiAssetResourceDelete(params: any) { - return request.post({ url: '/asset/AssetResource/delete', params }) + return request.post({ url: '/asset.AssetResource/delete', params }) } diff --git a/server/app/api/controller/asset/AssetAppController.php b/server/app/api/controller/asset/AssetAppController.php index 78f2ce42..ed1ba862 100644 --- a/server/app/api/controller/asset/AssetAppController.php +++ b/server/app/api/controller/asset/AssetAppController.php @@ -57,12 +57,12 @@ class AssetAppController extends BaseApiController { $token = $this->request->header('token'); if (empty($token)) { - return $this->fail('请先登录', [], 401); + return $this->fail('请先登录', [], -1); } $userId = cache('asset_token_' . $token); if (!$userId) { - return $this->fail('登录已过期', [], 401); + return $this->fail('登录已过期', [], -1); } $type = $this->request->get('type', 1); // 1:图片 2:视频 3:语音 @@ -95,4 +95,37 @@ class AssetAppController extends BaseApiController 'page_size' => $pageSize, ]); } + + /** + * @notes 修改密码 + */ + public function changePassword() + { + $token = $this->request->header('token'); + if (empty($token)) { + return $this->fail('请先登录', [], -1); + } + + $userId = cache('asset_token_' . $token); + if (!$userId) { + return $this->fail('登录已过期', [], -1); + } + + $oldPassword = $this->request->post('old_password'); + $password = $this->request->post('password'); + + $user = AssetUser::find($userId); + if (!$user) { + return $this->fail('用户不存在', [], -1); + } + + 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/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_system.sql b/server/sql/asset_system.sql index edd6c0e8..8f76dc62 100644 --- a/server/sql/asset_system.sql +++ b/server/sql/asset_system.sql @@ -1,4 +1,4 @@ -CREATE TABLE `asset_user` ( +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 '密码', @@ -9,7 +9,7 @@ CREATE TABLE `asset_user` ( UNIQUE KEY `uk_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私密资产-用户表'; -CREATE TABLE `asset_resource` ( +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 '资源名称/标题', @@ -20,7 +20,7 @@ CREATE TABLE `asset_resource` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='私密资产-资源表'; -CREATE TABLE `asset_user_resource` ( +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', From 44e97861d2cc5f4121b6406dac4769d402d9194a Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 20 May 2026 16:59:19 +0800 Subject: [PATCH 05/18] fix: use OSS direct upload for video and audio in resource admin --- admin/src/components/upload/index.vue | 8 +++++--- admin/src/views/asset/resource/index.vue | 17 ++++++++++++++++- .../app/common/service/DirectUploadService.php | 3 +++ 3 files changed, 24 insertions(+), 4 deletions(-) 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 index 992fac9e..0c034dd9 100644 --- a/admin/src/views/asset/resource/index.vue +++ b/admin/src/views/asset/resource/index.vue @@ -59,7 +59,16 @@ - + + + 上传视频 (OSS直传) + + + 上传语音 (OSS直传) + +
+ 已上传: {{ formData.file_url }} +
@@ -80,6 +89,7 @@ import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { apiAssetResourceList, apiAssetResourceAdd, apiAssetResourceDelete, apiAssetUserList } from '@/api/asset' import MaterialPicker from '@/components/material/picker.vue' +import Upload from '@/components/upload/index.vue' const loading = ref(false) const tableData = ref([]) @@ -143,6 +153,11 @@ const handleAdd = () => { dialogVisible.value = true } +const handleUploadSuccess = (response: any) => { + formData.file_url = response?.data?.uri || response?.data?.url || '' + ElMessage.success('上传成功') +} + const handleDelete = async (row: any) => { try { await ElMessageBox.confirm('确定要删除该资源吗?用户将无法再看到。', '提示', { type: 'warning' }) diff --git a/server/app/common/service/DirectUploadService.php b/server/app/common/service/DirectUploadService.php index aecf432b..cbe991bd 100755 --- a/server/app/common/service/DirectUploadService.php +++ b/server/app/common/service/DirectUploadService.php @@ -19,6 +19,7 @@ class DirectUploadService { /** 视频允许的扩展名(沿用 config/project.file_video) */ public const TYPE_VIDEO = 'video'; + public const TYPE_VOICE = 'voice'; /** 默认凭证有效期 30 分钟 */ public const DEFAULT_DURATION = 1800; @@ -26,6 +27,7 @@ class DirectUploadService /** 允许扩展类型 → 大小上限(字节) */ private const MAX_SIZE = [ self::TYPE_VIDEO => 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, }; } From 076cd0fcf9c5943b4c62deff1578fa0f5a20b83b Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 20 May 2026 17:13:26 +0800 Subject: [PATCH 06/18] fix: add changePassword to notNeedLogin for asset token auth --- server/app/api/controller/asset/AssetAppController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/app/api/controller/asset/AssetAppController.php b/server/app/api/controller/asset/AssetAppController.php index ed1ba862..31c5fab2 100644 --- a/server/app/api/controller/asset/AssetAppController.php +++ b/server/app/api/controller/asset/AssetAppController.php @@ -9,8 +9,8 @@ use think\facade\Db; class AssetAppController extends BaseApiController { - // 免登录验证的接口 - public array $notNeedLogin = ['login', 'getResourceList']; + // 免登录验证的接口(使用独立 asset_token 体系,在方法内自行校验) + public array $notNeedLogin = ['login', 'getResourceList', 'changePassword']; /** * @notes 小程序端登录 From ea8e6d7c1b21dba29280c517f6c7630c7c7f1f63 Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 20 May 2026 17:22:12 +0800 Subject: [PATCH 07/18] fix: switch asset token from file cache to database for reliability + always redirect on token invalid --- .../controller/asset/AssetAppController.php | 71 +++++++++++-------- server/sql/asset_user_token.sql | 4 ++ 2 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 server/sql/asset_user_token.sql diff --git a/server/app/api/controller/asset/AssetAppController.php b/server/app/api/controller/asset/AssetAppController.php index 31c5fab2..9388b2bb 100644 --- a/server/app/api/controller/asset/AssetAppController.php +++ b/server/app/api/controller/asset/AssetAppController.php @@ -9,9 +9,28 @@ use think\facade\Db; class AssetAppController extends BaseApiController { - // 免登录验证的接口(使用独立 asset_token 体系,在方法内自行校验) + // 所有接口都免框架登录验证(使用独立 asset_token 体系,在方法内自行校验) public array $notNeedLogin = ['login', 'getResourceList', 'changePassword']; + /** token 有效期:7 天 */ + const TOKEN_EXPIRE = 86400 * 7; + + /** + * @notes 通过 token 获取当前用户(数据库校验) + */ + private function getAssetUser(): ?AssetUser + { + $token = $this->request->header('token'); + if (empty($token)) { + return null; + } + $user = AssetUser::where('token', $token) + ->where('token_expire_time', '>', time()) + ->where('status', 1) + ->find(); + return $user ?: null; + } + /** * @notes 小程序端登录 */ @@ -37,9 +56,11 @@ class AssetAppController extends BaseApiController return $this->fail('密码错误'); } - // 简单生成 token (实际项目中建议用 JWT 或 Redis 存储 token) - $token = md5($user->id . time() . 'secret'); - cache('asset_token_' . $token, $user->id, 86400 * 7); + // 生成 token 并持久化到数据库 + $token = md5($user->id . time() . uniqid('asset', true)); + $user->token = $token; + $user->token_expire_time = time() + self::TOKEN_EXPIRE; + $user->save(); return $this->success('登录成功', [ 'token' => $token, @@ -55,23 +76,17 @@ class AssetAppController extends BaseApiController */ public function getResourceList() { - $token = $this->request->header('token'); - if (empty($token)) { - return $this->fail('请先登录', [], -1); + $user = $this->getAssetUser(); + if (!$user) { + return $this->fail('登录已过期,请重新登录', [], -1); } - $userId = cache('asset_token_' . $token); - if (!$userId) { - return $this->fail('登录已过期', [], -1); - } - - $type = $this->request->get('type', 1); // 1:图片 2:视频 3:语音 + $type = $this->request->get('type', 1); $pageNo = $this->request->get('page_no', 1); $pageSize = $this->request->get('page_size', 20); - // 获取用户关联的资源 ID 列表 - $resourceIds = AssetUserResource::where('user_id', $userId)->column('resource_id'); - + $resourceIds = AssetUserResource::where('user_id', $user->id)->column('resource_id'); + if (empty($resourceIds)) { return $this->data([ 'lists' => [], @@ -101,22 +116,22 @@ class AssetAppController extends BaseApiController */ public function changePassword() { - $token = $this->request->header('token'); - if (empty($token)) { - return $this->fail('请先登录', [], -1); - } - - $userId = cache('asset_token_' . $token); - if (!$userId) { - return $this->fail('登录已过期', [], -1); + $user = $this->getAssetUser(); + if (!$user) { + return $this->fail('登录已过期,请重新登录', [], -1); } $oldPassword = $this->request->post('old_password'); $password = $this->request->post('password'); - - $user = AssetUser::find($userId); - if (!$user) { - return $this->fail('用户不存在', [], -1); + + 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)) { 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`); From f639ef2fbb754be21b8071ecbd8b3123e994f699 Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 20 May 2026 17:38:28 +0800 Subject: [PATCH 08/18] feat: add time dimension filter (today/3d/7d/30d) to resource list --- .../app/api/controller/asset/AssetAppController.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server/app/api/controller/asset/AssetAppController.php b/server/app/api/controller/asset/AssetAppController.php index 9388b2bb..3913e606 100644 --- a/server/app/api/controller/asset/AssetAppController.php +++ b/server/app/api/controller/asset/AssetAppController.php @@ -84,6 +84,7 @@ class AssetAppController extends BaseApiController $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); $resourceIds = AssetUserResource::where('user_id', $user->id)->column('resource_id'); @@ -96,9 +97,14 @@ class AssetAppController extends BaseApiController ]); } - $count = AssetResource::whereIn('id', $resourceIds)->where('type', $type)->count(); - $lists = AssetResource::whereIn('id', $resourceIds) - ->where('type', $type) + $query = AssetResource::whereIn('id', $resourceIds)->where('type', $type); + 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(); From 8d015fa962e8da0109ce18128973021980d851ba Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 20 May 2026 17:54:21 +0800 Subject: [PATCH 09/18] fix: dual-write token (DB+cache) and dual-read with fallback, works with or without SQL migration --- .../controller/asset/AssetAppController.php | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/server/app/api/controller/asset/AssetAppController.php b/server/app/api/controller/asset/AssetAppController.php index 3913e606..9af5b66c 100644 --- a/server/app/api/controller/asset/AssetAppController.php +++ b/server/app/api/controller/asset/AssetAppController.php @@ -16,7 +16,7 @@ class AssetAppController extends BaseApiController const TOKEN_EXPIRE = 86400 * 7; /** - * @notes 通过 token 获取当前用户(数据库校验) + * @notes 通过 token 获取当前用户(DB 优先,file cache 兜底) */ private function getAssetUser(): ?AssetUser { @@ -24,11 +24,28 @@ class AssetAppController extends BaseApiController if (empty($token)) { return null; } - $user = AssetUser::where('token', $token) - ->where('token_expire_time', '>', time()) - ->where('status', 1) - ->find(); - return $user ?: null; + + // 方式1: 从数据库 token 字段查找(需要已执行 asset_user_token.sql) + try { + $user = AssetUser::where('token', $token) + ->where('token_expire_time', '>', time()) + ->where('status', 1) + ->find(); + if ($user) { + return $user; + } + } catch (\Throwable $e) { + // token 字段不存在时忽略,走 cache 兜底 + } + + // 方式2: 从 file cache 查找(兼容旧登录 / 未执行 SQL 迁移) + $userId = cache('asset_token_' . $token); + if ($userId) { + $user = AssetUser::where('id', $userId)->where('status', 1)->find(); + return $user ?: null; + } + + return null; } /** @@ -56,11 +73,20 @@ class AssetAppController extends BaseApiController return $this->fail('密码错误'); } - // 生成 token 并持久化到数据库 + // 生成 token 并双写(DB + cache 兜底) $token = md5($user->id . time() . uniqid('asset', true)); - $user->token = $token; - $user->token_expire_time = time() + self::TOKEN_EXPIRE; - $user->save(); + + // 写入 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, From 03b80310f60bcbe766b71641780ce8fc4142ed9d Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Wed, 20 May 2026 18:24:04 +0800 Subject: [PATCH 10/18] feat: add public resource support, usage filtering, and recordDownload API --- .../controller/asset/AssetAppController.php | 105 +++++++++++++++--- server/sql/asset_resource_usage.sql | 8 ++ 2 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 server/sql/asset_resource_usage.sql diff --git a/server/app/api/controller/asset/AssetAppController.php b/server/app/api/controller/asset/AssetAppController.php index 9af5b66c..a872bdc0 100644 --- a/server/app/api/controller/asset/AssetAppController.php +++ b/server/app/api/controller/asset/AssetAppController.php @@ -10,7 +10,7 @@ use think\facade\Db; class AssetAppController extends BaseApiController { // 所有接口都免框架登录验证(使用独立 asset_token 体系,在方法内自行校验) - public array $notNeedLogin = ['login', 'getResourceList', 'changePassword']; + public array $notNeedLogin = ['login', 'getResourceList', 'changePassword', 'recordDownload']; /** token 有效期:7 天 */ const TOKEN_EXPIRE = 86400 * 7; @@ -98,32 +98,53 @@ class AssetAppController extends BaseApiController } /** - * @notes 获取用户关联的资源列表 + * @notes 获取用户关联的资源列表 (或公开资源) */ public function getResourceList() { $user = $this->getAssetUser(); - if (!$user) { - return $this->fail('登录已过期,请重新登录', [], -1); - } - $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=已使用 - $resourceIds = AssetUserResource::where('user_id', $user->id)->column('resource_id'); + $query = AssetResource::where('type', $type); + $usedResourceIds = []; - if (empty($resourceIds)) { - return $this->data([ - 'lists' => [], - 'count' => 0, - 'page_no' => $pageNo, - 'page_size' => $pageSize, - ]); + if ($user) { + // 登录用户:只看自己的专属资源 + $resourceIds = AssetUserResource::where('user_id', $user->id)->column('resource_id'); + if (empty($resourceIds)) { + return $this->data(['lists' => [], 'count' => 0, 'page_no' => $pageNo, 'page_size' => $pageSize]); + } + $query = $query->whereIn('id', $resourceIds); + + // 获取该用户的使用记录 + $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]); + } + $query = $query->whereIn('id', $usedResourceIds); + } + } else { + // 游客(未登录):只看公开资源(没关联给任何用户的资源) + $associatedResourceIds = AssetUserResource::column('resource_id'); + if (!empty($associatedResourceIds)) { + $query = $query->whereNotIn('id', $associatedResourceIds); + } } - $query = AssetResource::whereIn('id', $resourceIds)->where('type', $type); + // 时间过滤 if ($days > 0) { $startTime = strtotime("-{$days} days", strtotime(date('Y-m-d'))); $query = $query->where('create_time', '>=', $startTime); @@ -133,7 +154,13 @@ class AssetAppController extends BaseApiController $lists = (clone $query) ->order('id', 'desc') ->page($pageNo, $pageSize) - ->select(); + ->select() + ->toArray(); + + // 附加 is_used 字段 + foreach ($lists as &$item) { + $item['is_used'] = in_array($item['id'], $usedResourceIds); + } return $this->data([ 'lists' => $lists, @@ -143,6 +170,52 @@ class AssetAppController extends BaseApiController ]); } + /** + * @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 修改密码 */ 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='私密资产-资源使用记录表'; From dad06304f192b88c72f07e55e8e9c0e0c9cc0d41 Mon Sep 17 00:00:00 2001 From: long <452591453@qq.com> Date: Thu, 21 May 2026 09:47:44 +0800 Subject: [PATCH 11/18] feat: make user_ids optional in admin resource form, unlinked resources become public --- admin/src/views/asset/resource/index.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin/src/views/asset/resource/index.vue b/admin/src/views/asset/resource/index.vue index 0c034dd9..3cf99159 100644 --- a/admin/src/views/asset/resource/index.vue +++ b/admin/src/views/asset/resource/index.vue @@ -71,9 +71,10 @@ - + +
不选择账号则资源为公开资源,所有人可见