# 一、 迁移文件

# 1.创建迁移文件

npx sequelize migration:generate --name=chat_complaint

# 2. 迁移文件代码

'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up(queryInterface, Sequelize) {
    const { STRING, INTEGER, TEXT, DATE, ENUM, TINYINT } = Sequelize;
    
    await queryInterface.createTable('chat_complaint', {
      id: {
        type: INTEGER,
        primaryKey: true,
        autoIncrement: true,
        comment: '主键ID'
      },
      user_id: {
        type: INTEGER,
        allowNull: false,
        comment: '投诉用户ID'
      },
      target_id: {
        type: INTEGER,
        allowNull: false,
        comment: '被投诉目标ID'
      },
      target_type: {
        type: ENUM('group', 'single'),
        allowNull: false,
        comment: '目标类型:group-群聊,single-用户'
      },
      target_name: {
        type: STRING(255),
        allowNull: true,
        defaultValue: '',
        comment: '被投诉目标名称'
      },
      reason: {
        type: STRING(500),
        allowNull: false,
        comment: '投诉原因'
      },
      content: {
        type: TEXT,
        allowNull: true,
        comment: '投诉详细内容'
      },
      images: {
        type: TEXT,
        allowNull: true,
        comment: '图片证据JSON数组'
      },
      complaint_chatinfo: {
        type: TEXT,
        allowNull: true,
        comment: '聊天证据JSON数组'
      },
      status: {
        type: TINYINT,
        allowNull: false,
        defaultValue: 0,
        comment: '状态:0-待处理,1-处理中,2-已处理,3-已关闭'
      },
      admin_reply: {
        type: TEXT,
        allowNull: true,
        comment: '管理员回复'
      },
      create_time: {
        type: DATE,
        allowNull: false,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
        comment: '创建时间'
      },
      update_time: {
        type: DATE,
        allowNull: false,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
        comment: '更新时间'
      },
      is_deleted: {
        type: TINYINT,
        allowNull: false,
        defaultValue: 0,
        comment: '是否删除:0-否,1-是'
      }
    }, {
      engine: 'InnoDB',
      charset: 'utf8mb4',
      comment: '投诉记录表',
      indexes: [
        {
          name: 'idx_user_id',
          fields: ['user_id']
        },
        {
          name: 'idx_target',
          fields: ['target_id', 'target_type']
        },
        {
          name: 'idx_status',
          fields: ['status']
        },
        {
          name: 'idx_create_time',
          fields: ['create_time']
        }
      ]
    });
  },

  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable('chat_complaint');
  }
};

# 3. 迁移命令

// 创建数据库表
npx sequelize db:migrate
// 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更
npx sequelize db:migrate:undo
// 可以通过 `db:migrate:undo:all` 回退到初始状态
npx sequelize db:migrate:undo:all

# 二、模型

/app/model/chat_complaint.js

'use strict';

module.exports = app => {
  const { STRING, INTEGER, TEXT, DATE, ENUM, TINYINT } = app.Sequelize;

  const ChatComplaint = app.model.define('chat_complaint', {
    id: {
      type: INTEGER,
      primaryKey: true,
      autoIncrement: true,
      comment: '主键ID'
    },
    user_id: {
      type: INTEGER,
      allowNull: false,
      comment: '投诉用户ID'
    },
    target_id: {
      type: INTEGER,
      allowNull: false,
      comment: '被投诉目标ID'
    },
    target_type: {
      type: ENUM('group', 'single'),
      allowNull: false,
      comment: '目标类型:group-群聊,single-用户'
    },
    target_name: {
      type: STRING(255),
      allowNull: true,
      defaultValue: '',
      comment: '被投诉目标名称'
    },
    reason: {
      type: STRING(500),
      allowNull: false,
      comment: '投诉原因'
    },
    content: {
      type: TEXT,
      allowNull: true,
      comment: '投诉详细内容'
    },
    images: {
      type: TEXT,
      allowNull: true,
      comment: '图片证据JSON数组',
      get() {
        const rawValue = this.getDataValue('images');
        return rawValue ? JSON.parse(rawValue) : [];
      },
      set(value) {
        this.setDataValue('images', value ? JSON.stringify(value) : '[]');
      }
    },
    complaint_chatinfo: {
      type: TEXT,
      allowNull: true,
      comment: '聊天证据JSON数组',
      get() {
        const rawValue = this.getDataValue('complaint_chatinfo');
        return rawValue ? JSON.parse(rawValue) : [];
      },
      set(value) {
        this.setDataValue('complaint_chatinfo', value ? JSON.stringify(value) : '[]');
      }
    },
    status: {
      type: TINYINT,
      allowNull: false,
      defaultValue: 0,
      comment: '状态:0-待处理,1-处理中,2-已处理,3-已关闭'
    },
    admin_reply: {
      type: TEXT,
      allowNull: true,
      comment: '管理员回复'
    },
    create_time: {
      type: DATE,
      allowNull: false,
      defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP'),
      comment: '创建时间'
    },
    update_time: {
      type: DATE,
      allowNull: false,
      defaultValue: app.Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'),
      comment: '更新时间'
    },
    is_deleted: {
      type: TINYINT,
      allowNull: false,
      defaultValue: 0,
      comment: '是否删除:0-否,1-是'
    }
  }, {
    tableName: 'chat_complaint',
    engine: 'InnoDB',
    charset: 'utf8mb4',
    comment: '投诉记录表',
    timestamps: false, // 不使用Sequelize的自动时间戳
    hooks: {
      beforeValidate(complaint) {
        if (!complaint.target_name) {
          complaint.target_name = '';
        }
        if (!complaint.images) {
          complaint.images = '[]';
        }
        if (!complaint.complaint_chatinfo) {
          complaint.complaint_chatinfo = '[]';
        }
      }
    }
  });

  // 定义关联关系
  ChatComplaint.associate = function() {
    // 投诉属于用户
    ChatComplaint.belongsTo(app.model.User, {
      foreignKey: 'user_id',
      targetKey: 'id',
      as: 'user'
    });
  };

  // 状态常量
  ChatComplaint.STATUS = {
    PENDING: 0,    // 待处理
    PROCESSING: 1, // 处理中
    RESOLVED: 2,   // 已处理
    CLOSED: 3      // 已关闭
  };

  // 类型常量
  ChatComplaint.TARGET_TYPE = {
    GROUP: 'group',
    SINGLE: 'single'
  };

  return ChatComplaint;
};

# 三、控制器

/app/controller/api/chat/complaint.js

'use strict';

const Controller = require('egg').Controller;
const fs = require('fs');
const path = require('path');

class ComplaintController extends Controller {
  // 创建投诉
  async create() {
    const { ctx } = this;
    try {
      const { target_id, target_type, target_name, reason, content, images, complaint_chatinfo } = ctx.request.body;
      const user_id = ctx.chat_user.id;
      
      // 验证必填字段
      if (!target_id || !target_type || !reason) {
        return ctx.apiFail({}, '缺少必要参数');
      }
      
      // 创建投诉记录
      const complaintData = {
        user_id,
        target_id: parseInt(target_id),
        target_type,
        target_name: target_name || '',
        reason,
        content: content || '',
        images: images || '[]',
        complaint_chatinfo: complaint_chatinfo || '[]',
        status: 0, // 0-待处理, 1-处理中, 2-已处理, 3-已关闭
        create_time: new Date(),
        update_time: new Date()
      };
      
      // 写入数据库
      const result = await ctx.service.complaint.create(complaintData);
      
      // 同时写入JSON文件备份
      await this.writeToJsonFile(complaintData, user_id, target_type);
      
      return ctx.apiSuccess({ id: result.id }, '投诉提交成功');
      
    } catch (error) {
      ctx.logger.error('创建投诉失败:', error);
      return ctx.apiFail({}, '投诉提交失败');
    }
  }
  
  // 获取投诉列表
  async list() {
    const { ctx } = this;
    try {
      const user_id = ctx.chat_user.id;
      const complaints = await ctx.service.complaint.getByUserId(user_id);
      
      // 格式化返回数据
      const formattedComplaints = complaints.map(item => {
        const complaint = item.toJSON();
        return {
          ...complaint,
          user: complaint.user ? {
            id: complaint.user.id,
            uuid: complaint.user.uuid,
            username: complaint.user.username,
            nickname: complaint.user.nickname,
            avatar: complaint.user.avatar
          } : null
        };
      });
      
      return ctx.apiSuccess(formattedComplaints, '获取成功');
      
    } catch (error) {
      ctx.logger.error('获取投诉列表失败:', error);
      return ctx.apiFail({}, '获取失败');
    }
  }
  
  // 获取投诉详情
  async detail() {
    const { ctx } = this;
    try {
      const { id } = ctx.params;
      const user_id = ctx.chat_user.id;
      
      const complaint = await ctx.service.complaint.getById(id, user_id);
      
      if (!complaint) {
        return ctx.apiFail({}, '投诉记录不存在');
      }
      
      // 格式化返回数据
      const formattedComplaint = complaint.toJSON();
      const result = {
        ...formattedComplaint,
        user: formattedComplaint.user ? {
          id: formattedComplaint.user.id,
          uuid: formattedComplaint.user.uuid,
          username: formattedComplaint.user.username,
          nickname: formattedComplaint.user.nickname,
          avatar: formattedComplaint.user.avatar
        } : null
      };
      
      return ctx.apiSuccess(result, '获取成功');
      
    } catch (error) {
      ctx.logger.error('获取投诉详情失败:', error);
      return ctx.apiFail({}, '获取失败');
    }
  }
  
  // 更新投诉
  async update() {
    const { ctx } = this;
    try {
      const { id } = ctx.params;
      const { content, images } = ctx.request.body;
      const user_id = ctx.chat_user.id;
      
      // 检查投诉记录是否存在且属于当前用户
      const complaint = await ctx.service.complaint.getById(id, user_id);
      if (!complaint) {
        return ctx.apiFail({}, '投诉记录不存在');
      }
      
      // 只能更新待处理的投诉
      if (complaint.status !== 0) {
        return ctx.apiFail({}, '该投诉已开始处理,无法修改');
      }
      
      const updateData = {
        content: content || complaint.content,
        images: images || complaint.images,
        update_time: new Date()
      };
      
      const success = await ctx.service.complaint.update(id, updateData, user_id);
      
      if (!success) {
        return ctx.apiFail({}, '更新失败');
      }
      
      return ctx.apiSuccess({}, '更新成功');
      
    } catch (error) {
      ctx.logger.error('更新投诉失败:', error);
      return ctx.apiFail({}, '更新失败');
    }
  }
  
  // 删除投诉
  async delete() {
    const { ctx } = this;
    try {
      const { id } = ctx.params;
      const user_id = ctx.chat_user.id;
      
      // 检查投诉记录是否存在且属于当前用户
      const complaint = await ctx.service.complaint.getById(id, user_id);
      if (!complaint) {
        return ctx.apiFail({}, '投诉记录不存在');
      }
      
      const success = await ctx.service.complaint.delete(id, user_id);
      
      if (!success) {
        return ctx.apiFail({}, '删除失败');
      }
      
      return ctx.apiSuccess({}, '删除成功');
      
    } catch (error) {
      ctx.logger.error('删除投诉失败:', error);
      return ctx.apiFail({}, '删除失败');
    }
  }
  
  // 管理员更新投诉状态
  async updateStatus() {
    const { ctx } = this;
    try {
      const { id } = ctx.params;
      const { status, admin_reply } = ctx.request.body;
      
      // 检查用户权限
      if (ctx.chat_user.role !== 'admin') {
        return ctx.apiFail({}, '无权限操作');
      }
      
      const success = await ctx.service.complaint.updateStatus(id, status, admin_reply);
      
      if (!success) {
        return ctx.apiFail({}, '更新状态失败');
      }
      
      return ctx.apiSuccess({}, '更新成功');
      
    } catch (error) {
      ctx.logger.error('更新投诉状态失败:', error);
      return ctx.apiFail({}, '更新失败');
    }
  }
  
  // 获取所有投诉(管理员用)
  async listAll() {
    const { ctx } = this;
    try {
      const { page = 1, pageSize = 20 } = ctx.query;
      
      // 检查用户权限
      if (ctx.chat_user.role !== 'admin') {
        return ctx.apiFail({}, '无权限查看');
      }
      
      const result = await ctx.service.complaint.getAllComplaints(parseInt(page), parseInt(pageSize));
      
      // 格式化返回数据
      const formattedComplaints = result.rows.map(item => {
        const complaint = item.toJSON();
        return {
          ...complaint,
          user: complaint.user ? {
            id: complaint.user.id,
            uuid: complaint.user.uuid,
            username: complaint.user.username,
            nickname: complaint.user.nickname,
            avatar: complaint.user.avatar
          } : null
        };
      });
      
      return ctx.apiSuccess({
        list: formattedComplaints,
        total: result.count,
        page: parseInt(page),
        pageSize: parseInt(pageSize)
      }, '获取成功');
      
    } catch (error) {
      ctx.logger.error('获取所有投诉失败:', error);
      return ctx.apiFail({}, '获取失败');
    }
  }
  
  // 写入JSON文件
  async writeToJsonFile(complaintData, userId, targetType) {
    try {
      const dataDir = path.join(this.app.baseDir, 'data', 'complaint');
      
      // 确保目录存在
      if (!fs.existsSync(dataDir)) {
        fs.mkdirSync(dataDir, { recursive: true });
      }
      
      const filename = `chat${targetType}_${userId}.json`;
      const filepath = path.join(dataDir, filename);
      
      let existingData = [];
      
      // 如果文件已存在,读取现有数据
      if (fs.existsSync(filepath)) {
        const fileContent = fs.readFileSync(filepath, 'utf8');
        existingData = JSON.parse(fileContent);
      }
      
      // 添加新投诉记录
      existingData.push({
        ...complaintData,
        id: complaintData.id || Date.now() // 使用数据库ID或临时ID
      });
      
      // 写入文件
      fs.writeFileSync(filepath, JSON.stringify(existingData, null, 2), 'utf8');
      
    } catch (error) {
      this.ctx.logger.error('写入JSON文件失败:', error);
    }
  }
}

module.exports = ComplaintController;

# 四、服务

/app/service/complaint.js

'use strict';

const Service = require('egg').Service;

class ComplaintService extends Service {
  // 创建投诉
  async create(complaintData) {
    return await this.ctx.model.ChatComplaint.create(complaintData);
  }
  
  // 根据用户ID获取投诉列表(包含用户信息)
  async getByUserId(userId) {
    return await this.ctx.model.ChatComplaint.findAll({
      where: {
        user_id: userId,
        is_deleted: 0
      },
      attributes: [
        'id', 'target_id', 'target_type', 'target_name', 'reason', 
        'content', 'images', 'status', 'admin_reply', 'create_time', 'update_time'
      ],
      include: [
        {
          model: this.app.model.User,
          as: 'user',
          attributes: ['id', 'uuid', 'username', 'nickname', 'avatar']
        }
      ],
      order: [[ 'create_time', 'DESC' ]]
    });
  }
  
  // 根据ID获取投诉详情(包含用户信息)
  async getById(id, userId) {
    return await this.ctx.model.ChatComplaint.findOne({
      where: {
        id,
        user_id: userId,
        is_deleted: 0
      },
      attributes: [
        'id', 'target_id', 'target_type', 'target_name', 'reason', 
        'content', 'images', 'status', 'admin_reply', 'create_time', 'update_time'
      ],
      include: [
        {
          model: this.app.model.User,
          as: 'user',
          attributes: ['id', 'uuid', 'username', 'nickname', 'avatar']
        }
      ]
    });
  }
  
  // 获取所有投诉(管理员用)
  async getAllComplaints(page = 1, pageSize = 20) {
    const offset = (page - 1) * pageSize;
    
    return await this.ctx.model.ChatComplaint.findAndCountAll({
      where: {
        is_deleted: 0
      },
      attributes: [
        'id', 'target_id', 'target_type', 'target_name', 'reason', 
        'content', 'images', 'status', 'admin_reply', 'create_time', 'update_time'
      ],
      include: [
        {
          model: this.app.model.User,
          as: 'user',
          attributes: ['id', 'uuid', 'username', 'nickname', 'avatar']
        }
      ],
      order: [[ 'create_time', 'DESC' ]],
      offset,
      limit: pageSize
    });
  }
  
  // 更新投诉
  async update(id, updateData, userId) {
    const where = { id };
    if (userId) {
      where.user_id = userId;
    }
    
    const result = await this.ctx.model.ChatComplaint.update(updateData, {
      where
    });
    return result[0] > 0;
  }
  
  // 删除投诉(软删除)
  async delete(id, userId) {
    const where = { id };
    if (userId) {
      where.user_id = userId;
    }
    
    const result = await this.ctx.model.ChatComplaint.update({
      is_deleted: 1,
      update_time: new Date()
    }, {
      where
    });
    return result[0] > 0;
  }
  
  // 更新投诉状态(管理员用)
  async updateStatus(id, status, adminReply = '') {
    const updateData = {
      status,
      update_time: new Date()
    };
    
    if (adminReply) {
      updateData.admin_reply = adminReply;
    }
    
    const result = await this.ctx.model.ChatComplaint.update(updateData, {
      where: { id }
    });
    return result[0] > 0;
  }
}

module.exports = ComplaintService;

# 五、路由

/app/router/api/chat/router.js

    // 投诉相关路由
    router.post('/api/chat/complaint/create', controller.api.chat.complaint.create);
    router.get('/api/chat/complaint/list', controller.api.chat.complaint.list);
    router.get('/api/chat/complaint/detail/:id', controller.api.chat.complaint.detail);
    router.post('/api/chat/complaint/update/:id', controller.api.chat.complaint.update);
    router.post('/api/chat/complaint/delete/:id', controller.api.chat.complaint.delete);
    
    // 管理员路由
    // router.post('/admin/chat/complaint/status/:id', controller.api.chat.complaint.updateStatus);
    // router.get('/admin/chat/complaint/listAll', controller.api.chat.complaint.listAll);
更新时间: 2025年10月12日星期日中午12点22分