Explorar el Código

✨ feat(mobile): 实现银龄人才和岗位推荐功能

- 添加银龄人才和岗位推荐模块,丰富首页内容
- 创建Unsplash图片工具类,提供高质量图片资源
- 优化数据转换逻辑,增加空值处理和默认描述

✨ feat(server): 新增银龄人才和岗位推荐API

- 实现getSilverTalents方法,基于用户画像推荐人才
- 添加getSilverPositions方法,筛选银龄特色岗位
- 扩展HomeData接口,包含silverTalents和silverPositions字段

♻️ refactor(server): 优化用户头像URL长度限制

- 修改avatarUrl字段长度限制,支持完整URL存储
- 调整相关DTO和Schema定义,保持数据验证一致性
yourname hace 7 meses
padre
commit
dcdd179987

+ 28 - 5
src/client/mobile/pages/NewHomePage.tsx

@@ -5,6 +5,7 @@ import { useAuth } from '../hooks/AuthProvider';
 import { EnhancedCarousel } from '../components/EnhancedCarousel';
 import { AdBannerCarousel } from '../components/AdBannerCarousel';
 import { SkeletonLoader, BannerSkeleton, ListItemSkeleton } from '../components/SkeletonLoader';
+import { unsplash } from '../utils/unsplash';
 import {
   BriefcaseIcon,
   UserGroupIcon,
@@ -92,12 +93,12 @@ const serviceCategories = [
 ];
 
 // 数据转换工具
-const transformPolicyNews = (news: any[]) => 
+const transformPolicyNews = (news: any[]) =>
   news.map(item => ({
     id: item.id,
     title: item.newsTitle,
-    description: item.summary || item.newsContent.substring(0, 100) + '...',
-    image: item.images?.split(',')[0] || '/images/banner1.jpg',
+    description: item.summary || item.newsContent?.substring(0, 100) + '...' || '暂无描述',
+    image: item.images?.split(',')[0] || unsplash.getPlaceholderImage('community', item.id),
     fallbackImage: '/images/placeholder-banner.jpg',
     link: `/policy-news/${item.id}`
   }));
@@ -108,7 +109,7 @@ const transformJobs = (jobs: any[]) =>
     title: job.title,
     company: job.company?.name || '未知公司',
     salary: job.salaryRange || '面议',
-    image: job.company?.logo || `https://picsum.photos/seed/${job.id}/200/200`,
+    image: job.company?.logo || unsplash.getJobImage(job.id),
     tags: [job.location ? job.location.split(' ')[0] : '全国', '热门'],
     createdAt: job.createdAt
   }));
@@ -118,7 +119,7 @@ const transformKnowledge = (knowledge: any[]) =>
     id: item.id,
     title: item.title,
     category: item.category?.name || '其他',
-    coverImage: item.coverImage || `https://picsum.photos/seed/${item.id}/200/200`,
+    coverImage: item.coverImage || unsplash.getEducationImage(item.id),
     viewCount: item.viewCount || 0,
     createdAt: item.createdAt
   }));
@@ -133,6 +134,28 @@ const transformTimeBank = (activities: any[]) =>
     workDate: new Date(activity.workDate).toLocaleDateString()
   }));
 
+const transformSilverTalents = (talents: any[]) =>
+  talents.map(talent => ({
+    id: talent.id,
+    name: talent.user?.username || talent.realName || '匿名用户',
+    specialty: talent.personalSkills || talent.profession || '暂无特长',
+    city: talent.city || '未知地区',
+    createdAt: talent.createdAt,
+    avatar: unsplash.getElderlyAvatar(talent.userId || talent.id)
+  }));
+
+const transformSilverPositions = (positions: any[]) =>
+  positions.map(position => ({
+    id: position.id,
+    title: position.title,
+    organization: position.company?.name || '未知单位',
+    budget: position.salaryRange || '面议',
+    deadline: position.deadline || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
+    location: position.location || '全国',
+    category: position.category?.name || '其他',
+    createdAt: position.createdAt
+  }));
+
 // 主页面组件
 const NewHomePage: React.FC = () => {
   const navigate = useNavigate();

+ 137 - 0
src/client/mobile/utils/unsplash.ts

@@ -0,0 +1,137 @@
+/**
+ * Unsplash 图片工具类
+ * 为银龄智慧应用提供高质量的图片资源
+ */
+export class UnsplashImages {
+  // Unsplash 主题分类
+  static themes = {
+    elderly: {
+      people: [
+        'https://images.unsplash.com/photo-1442458370899-ae20e367c5d8?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1556889882-73ea40694a98?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1581579438747-104c53d7fbc4?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop'
+      ],
+      jobs: [
+        'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1600880292203-757bb62b4baf?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1551434678-e076c223a692?w=400&h=400&fit=crop'
+      ],
+      education: [
+        'https://images.unsplash.com/photo-1523050854058-8df90110c9f1?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1503676260728-1c00da094a0b?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?w=400&h=400&fit=crop'
+      ],
+      health: [
+        'https://images.unsplash.com/photo-1576091160399-112ba8d25d1f?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=400&fit=crop',
+        'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400&h=400&fit=crop'
+      ]
+    },
+    nature: [
+      'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400&h=400&fit=crop',
+      'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=400&h=400&fit=crop',
+      'https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=400&h=400&fit=crop'
+    ],
+    community: [
+      'https://images.unsplash.com/photo-1517486808906-6ca8b3f8e1c1?w=400&h=400&fit=crop',
+      'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=400&fit=crop',
+      'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?w=400&h=400&fit=crop'
+    ]
+  };
+
+  /**
+   * 获取银龄人物头像
+   * @param seed 用于生成唯一图片的种子
+   * @returns Unsplash 图片URL
+   */
+  static getElderlyAvatar(seed: string | number): string {
+    const index = this.hashCode(seed.toString()) % this.themes.elderly.people.length;
+    return this.themes.elderly.people[index];
+  }
+
+  /**
+   * 获取工作相关图片
+   * @param seed 用于生成唯一图片的种子
+   * @returns Unsplash 图片URL
+   */
+  static getJobImage(seed: string | number): string {
+    const index = this.hashCode(seed.toString()) % this.themes.elderly.jobs.length;
+    return this.themes.elderly.jobs[index];
+  }
+
+  /**
+   * 获取教育相关图片
+   * @param seed 用于生成唯一图片的种子
+   * @returns Unsplash 图片URL
+   */
+  static getEducationImage(seed: string | number): string {
+    const index = this.hashCode(seed.toString()) % this.themes.elderly.education.length;
+    return this.themes.elderly.education[index];
+  }
+
+  /**
+   * 获取健康相关图片
+   * @param seed 用于生成唯一图片的种子
+   * @returns Unsplash 图片URL
+   */
+  static getHealthImage(seed: string | number): string {
+    const index = this.hashCode(seed.toString()) % this.themes.elderly.health.length;
+    return this.themes.elderly.health[index];
+  }
+
+  /**
+   * 获取通用占位图片
+   * @param category 图片类别
+   * @param seed 用于生成唯一图片的种子
+   * @returns Unsplash 图片URL
+   */
+  static getPlaceholderImage(category: 'nature' | 'community' = 'nature', seed: string | number = ''): string {
+    const theme = this.themes[category];
+    if (Array.isArray(theme)) {
+      const index = seed ? this.hashCode(seed.toString()) % theme.length : 0;
+      return theme[index];
+    }
+    return this.themes.nature[0];
+  }
+
+  /**
+   * 生成哈希码用于图片选择
+   * @param str 输入字符串
+   * @returns 哈希值
+   */
+  private static hashCode(str: string): number {
+    let hash = 0;
+    for (let i = 0; i < str.length; i++) {
+      const char = str.charCodeAt(i);
+      hash = ((hash << 5) - hash) + char;
+      hash = hash & hash; // 转换为32位整数
+    }
+    return Math.abs(hash);
+  }
+
+  /**
+   * 获取带尺寸的图片URL
+   * @param url 基础URL
+   * @param width 宽度
+   * @param height 高度
+   * @returns 带尺寸参数的URL
+   */
+  static resizeImage(url: string, width: number, height: number): string {
+    return url.replace(/w=\d+&h=\d+/, `w=${width}&h=${height}`);
+  }
+
+  /**
+   * 获取模糊占位图片(用于懒加载)
+   * @param url 原始URL
+   * @returns 模糊版本URL
+   */
+  static getBlurPlaceholder(url: string): string {
+    return url.includes('?') ? `${url}&q=10&blur=10` : `${url}?q=10&blur=10`;
+  }
+}
+
+// 导出实例以便直接使用
+export const unsplash = UnsplashImages;

+ 4 - 0
src/server/api/home/index.ts

@@ -12,6 +12,8 @@ const HomeResponse = z.object({
   recommendedJobs: z.array(z.any()),
   hotKnowledge: z.array(z.any()),
   timeBankActivities: z.array(z.any()),
+  silverTalents: z.array(z.any()),
+  silverPositions: z.array(z.any()),
   userStats: z.object({
     pointBalance: z.number(),
     timeBankHours: z.number(),
@@ -111,6 +113,8 @@ const app = new OpenAPIHono<AuthContext>()
     homeData.recommendedJobs = homeData.recommendedJobs.slice(0, Math.min(limit, 6));
     homeData.hotKnowledge = homeData.hotKnowledge.slice(0, Math.min(limit, 4));
     homeData.timeBankActivities = homeData.timeBankActivities.slice(0, Math.min(limit, 3));
+    homeData.silverTalents = homeData.silverTalents.slice(0, Math.min(limit, 3));
+    homeData.silverPositions = homeData.silverPositions.slice(0, Math.min(limit, 3));
 
     return c.json(homeData as any, 200);
   } catch (error) {

+ 81 - 0
src/server/modules/home/home.service.ts

@@ -15,6 +15,8 @@ export interface HomeData {
   recommendedJobs: Job[];
   hotKnowledge: SilverKnowledge[];
   timeBankActivities: SilverTimeBank[];
+  silverTalents: SilverUserProfile[];
+  silverPositions: Job[];
   userStats: {
     pointBalance: number;
     timeBankHours: number;
@@ -67,12 +69,16 @@ export class HomeService {
       recommendedJobs,
       hotKnowledge,
       timeBankActivities,
+      silverTalents,
+      silverPositions,
       userStats
     ] = await Promise.all([
       this.getBanners(),
       this.getRecommendedJobs(userId),
       this.getHotKnowledge(userId),
       this.getTimeBankActivities(userId),
+      this.getSilverTalents(userId),
+      this.getSilverPositions(userId),
       this.getUserStats(userId)
     ]);
 
@@ -81,6 +87,8 @@ export class HomeService {
       recommendedJobs,
       hotKnowledge,
       timeBankActivities,
+      silverTalents,
+      silverPositions,
       userStats
     };
   }
@@ -267,6 +275,79 @@ export class HomeService {
     };
   }
 
+  /**
+   * 获取银龄人才(基于用户画像推荐)
+   */
+  async getSilverTalents(userId?: number, limit: number = 3): Promise<SilverUserProfile[]> {
+    if (!userId) {
+      // 未登录用户:返回最新注册的人才
+      return await this.userProfileRepo
+        .createQueryBuilder('profile')
+        .leftJoinAndSelect('profile.user', 'user')
+        .where('profile.personalSkills IS NOT NULL')
+        .andWhere('profile.personalSkills != :empty', { empty: '' })
+        .orderBy('profile.createdAt', 'DESC')
+        .limit(limit)
+        .getMany();
+    }
+
+    // 登录用户:基于技能匹配推荐
+    const userProfile = await this.userProfileRepo.findOne({ where: { userId } });
+    const userSkills = userProfile?.personalSkills?.split(',').map(s => s.trim()).filter(s => s) || [];
+
+    const queryBuilder = this.userProfileRepo
+      .createQueryBuilder('profile')
+      .leftJoinAndSelect('profile.user', 'user')
+      .where('profile.userId != :userId', { userId })
+      .andWhere('profile.personalSkills IS NOT NULL')
+      .andWhere('profile.personalSkills != :empty', { empty: '' });
+
+    if (userSkills.length > 0) {
+      queryBuilder.andWhere(
+        new Brackets(qb => {
+          userSkills.forEach((skill, index) => {
+            if (index === 0) {
+              qb.where('profile.personalSkills LIKE :skill', { skill: `%${skill}%` });
+            } else {
+              qb.orWhere('profile.personalSkills LIKE :skill', { skill: `%${skill}%` });
+            }
+          });
+        })
+      );
+    }
+
+    return await queryBuilder
+      .orderBy('profile.createdAt', 'DESC')
+      .limit(limit)
+      .getMany();
+  }
+
+  /**
+   * 获取银龄岗位(银龄特色岗位推荐)
+   */
+  async getSilverPositions(userId?: number, limit: number = 3): Promise<Job[]> {
+    // 筛选银龄特色岗位(特别适合银龄群体的岗位)
+    return await this.jobRepo
+      .createQueryBuilder('job')
+      .leftJoinAndSelect('job.company', 'company')
+      .where('job.status = :status', { status: 1 })
+      .andWhere(
+        new Brackets(qb => {
+          qb.where('job.title LIKE :silver', { silver: '%银龄%' })
+            .orWhere('job.title LIKE :senior', { senior: '%资深%' })
+            .orWhere('job.title LIKE :expert', { expert: '%专家%' })
+            .orWhere('job.title LIKE :elderly', { elderly: '%老年%' })
+            .orWhere('job.description LIKE :silver', { silver: '%银龄%' })
+            .orWhere('job.description LIKE :senior', { senior: '%资深%' })
+            .orWhere('job.description LIKE :expert', { expert: '%专家%' })
+            .orWhere('job.description LIKE :elderly', { elderly: '%老年%' });
+        })
+      )
+      .orderBy('job.createdAt', 'DESC')
+      .limit(limit)
+      .getMany();
+  }
+
   /**
    * 搜索功能(全局搜索岗位、知识、企业)
    */

+ 4 - 4
src/server/modules/silver-users/silver-user-profile.entity.ts

@@ -55,7 +55,7 @@ export class SilverUserProfile {
   @Column({ name: 'email', type: 'varchar', length: 255, nullable: true })
   email!: string | null;
 
-  @Column({ name: 'avatar_url', type: 'varchar', length: 500, nullable: true })
+  @Column({ name: 'avatar_url', type: 'text', nullable: true })
   avatarUrl!: string | null;
 
   @Column({ name: 'personal_intro', type: 'text', nullable: true })
@@ -146,7 +146,7 @@ export const SilverUserProfileSchema = z.object({
   gender: z.number().int().min(1).max(3).openapi({ description: '性别:1-男,2-女,3-其他', example: 1 }),
   phone: z.string().max(20).openapi({ description: '联系电话', example: '13800138000' }),
   email: z.string().max(255).email().nullable().optional().openapi({ description: '电子邮箱', example: 'example@email.com' }),
-  avatarUrl: z.string().max(500).url().nullable().optional().openapi({ description: '头像URL', example: 'https://example.com/avatar.jpg' }),
+  avatarUrl: z.string().url().nullable().optional().openapi({ description: '头像URL', example: 'https://example.com/avatar.jpg' }),
   personalIntro: z.string().nullable().optional().openapi({ description: '个人简介', example: '退休教师,热爱教育事业' }),
   personalSkills: z.string().nullable().optional().openapi({ description: '个人技能', example: '书法、绘画、音乐教学' }),
   personalExperience: z.string().nullable().optional().openapi({ description: '个人经历', example: '从事教育工作40年,经验丰富' }),
@@ -181,7 +181,7 @@ export const CreateSilverUserProfileDto = z.object({
   nickname: z.string().max(50).optional().openapi({ description: '昵称', example: '张大爷' }),
   organization: z.string().max(255).optional().openapi({ description: '所属组织/机构', example: '社区服务中心' }),
   email: z.string().max(255).email().optional().openapi({ description: '电子邮箱', example: 'example@email.com' }),
-  avatarUrl: z.string().max(500).url().optional().openapi({ description: '头像URL', example: 'https://example.com/avatar.jpg' }),
+  avatarUrl: z.string().url().optional().openapi({ description: '头像URL', example: 'https://example.com/avatar.jpg' }),
   personalIntro: z.string().optional().openapi({ description: '个人简介', example: '退休教师,热爱教育事业' }),
   personalSkills: z.string().optional().openapi({ description: '个人技能', example: '书法、绘画、音乐教学' }),
   personalExperience: z.string().optional().openapi({ description: '个人经历', example: '从事教育工作40年,经验丰富' }),
@@ -197,7 +197,7 @@ export const UpdateSilverUserProfileDto = z.object({
   nickname: z.string().max(50).optional().openapi({ description: '昵称', example: '张大爷' }),
   organization: z.string().max(255).optional().openapi({ description: '所属组织/机构', example: '社区服务中心' }),
   email: z.string().max(255).email().optional().openapi({ description: '电子邮箱', example: 'example@email.com' }),
-  avatarUrl: z.string().max(500).url().optional().openapi({ description: '头像URL', example: 'https://example.com/avatar.jpg' }),
+  avatarUrl: z.string().url().optional().openapi({ description: '头像URL', example: 'https://example.com/avatar.jpg' }),
   personalIntro: z.string().optional().openapi({ description: '个人简介', example: '退休教师,热爱教育事业' }),
   personalSkills: z.string().optional().openapi({ description: '个人技能', example: '书法、绘画、音乐教学' }),
   personalExperience: z.string().optional().openapi({ description: '个人经历', example: '从事教育工作40年,经验丰富' }),