Bootstrap

QT:动态生成验证码

captchamovablelabel.h

#ifndef CAPTCHAMOVABLELABEL_H
#define CAPTCHAMOVABLELABEL_H

#include <QLabel>
#include <QTime>
#include <QPropertyAnimation>
#include <QDebug>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QApplication>
#include <QGraphicsDropShadowEffect>
#include <cmath>
#include <QTimer>

#define CAPTCHA_REFRESH_DURATION 300 // 刷新的动画时长
#define CAPTCHA_CHAR_ANGLE_MAX 20 // 最大旋转角:20°
#define CAPTCHA_SHADOW_BLUR_MAX 80 // 最大的阴影模糊半径

class CaptchaMovableLabel : public QLabel
{
    Q_OBJECT
    Q_PROPERTY(int refreshProgress READ getRefreshProgress WRITE setRefreshProgress)
    Q_PROPERTY(int pressProgress READ getPressProgress WRITE setPressProgress)
public:
    CaptchaMovableLabel(QWidget* parent);

    void setAngle(int angle);
    void setColor(QColor color);
    void setText(QString ch);
    void startRefreshAnimation();
    void setMoveBorder(QRect rect);

    QString text();

protected:
    void paintEvent(QPaintEvent *) override;
    void mousePressEvent(QMouseEvent *ev) override;
    void mouseMoveEvent(QMouseEvent *ev) override;
    void mouseReleaseEvent(QMouseEvent *ev) override;

private:
    void startPressAnimation(int end);
    void setRefreshProgress(int g);
    int getRefreshProgress();
    inline bool isNoAni();
    void setPressProgress(int g);
    int getPressProgress();

private slots:
    //设置rgb颜色
    void slotMovePos();

private:
    QPoint press_pos;
    bool dragging =  false;
    bool moved = false;
    QGraphicsDropShadowEffect effect;

    QString ch;
    QColor color;
    int angle = 0;
    int refreshProgress = 100;

    QString prevCh;
    QColor prevColor;
    int prevAngle = 0;
    QString prevChar;

    int pressProgress = 0;

    bool inited = false;
    QTimer movingTimer;
    int moveR, moveG, moveB;
};

#endif // CAPTCHAMOVABLELABEL_H


captchamovablelabel.cpp

#include "captchamovablelabel.h"

CaptchaMovableLabel::CaptchaMovableLabel(QWidget *parent) : QLabel(parent)
{
    effect.setOffset(0, 0);
//    effect.setBlurRadius(8);
    setGraphicsEffect(&effect);

    movingTimer.setInterval(30);
    movingTimer.setSingleShot(false);
    connect(&movingTimer, SIGNAL(timeout()), this, SLOT(slotMovePos()));
}

void CaptchaMovableLabel::setAngle(int angle)
{
    this->prevAngle = this->angle;
    this->angle = angle;
}

void CaptchaMovableLabel::setColor(QColor color)
{
    this->prevColor = this->color;
    this->color = color;

    moveR = qrand() % 5;
    moveG = qrand() % 5;
    moveB = qrand() % 5;
    movingTimer.start();
}

void CaptchaMovableLabel::setText(QString text)
{
    this->prevCh = this->ch;
    this->ch = text;

    // 计算合适的高度
    QFontMetrics fm(this->font());
    double w = fm.horizontalAdvance(text)+2;
    double h = fm.height();

    const double PI = 3.141592;
    int xieHalf = sqrt(w*w/4+h*h/4); // 斜边的一半
    double a = atan(w/h) + CAPTCHA_CHAR_ANGLE_MAX * PI / 180; // 最大的倾斜角度
    int w2 = xieHalf * sin(a) * 2;

    a = atan(w/h) - CAPTCHA_CHAR_ANGLE_MAX * PI / 180;
    int h2 = xieHalf * cos(a) * 2;

    resize(w2, h2);
}

void CaptchaMovableLabel::startRefreshAnimation()
{
    if (!inited) // 第一次,直接显示,取消动画
    {
        inited = true;
        return ;
    }

    QPropertyAnimation* ani = new QPropertyAnimation(this, "refreshProgress");
    ani->setStartValue(0);
    ani->setEndValue(100);
    ani->setDuration(qrand() % (CAPTCHA_REFRESH_DURATION / 3) + CAPTCHA_REFRESH_DURATION / 3);
    ani->start();
    connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
    connect(ani, SIGNAL(finished()), &movingTimer, SLOT(start()));
}

QString CaptchaMovableLabel::text()
{
    return ch;
}

void CaptchaMovableLabel::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setFont(this->font());
    painter.setRenderHint(QPainter::SmoothPixmapTransform);

    int w2 = width()/2, h2 = height()/2;
    painter.translate(w2, h2); // 平移到中心,绕中心点旋转

    if (isNoAni()) // 不在动画中,直接绘制
    {
        painter.setPen(color);
        painter.rotate(angle);
        painter.drawText(QRect(-w2, -h2, width(), height()), Qt::AlignCenter, ch);
        return ;
    }

    // 动画里面,前后渐变替换
    double newProp = refreshProgress / 100.0;
    double oldProp = 1.0 - newProp;

    double a = prevAngle * oldProp + angle * newProp + 0.5;
    painter.save();
    painter.rotate(a);

    QColor c = prevColor;
    c.setAlpha(c.alpha() * oldProp); // 旧文字渐渐消失
    painter.setPen(c);
    painter.drawText(QRect(-w2,-h2,width(),height()), Qt::AlignCenter, prevCh);

    c = this->color;
    c.setAlpha(c.alpha() * newProp); // 新文字渐渐显示
    painter.setPen(c);
    painter.drawText(QRect(-w2, -h2, width(), height()), Qt::AlignCenter, ch);
    painter.restore();
}

void CaptchaMovableLabel::mousePressEvent(QMouseEvent *ev)
{
    if (ev->button() == Qt::LeftButton)
    {
        // 开始拖拽
        press_pos = ev->pos();
        dragging = true;
        moved = false;
        this->raise();
        movingTimer.stop();

        startPressAnimation(200);

        return ev->accept();
    }
    QLabel::mousePressEvent(ev);
}

void CaptchaMovableLabel::mouseMoveEvent(QMouseEvent *ev)
{
    if (dragging && ev->buttons() & Qt::LeftButton)
    {
        if (!moved && (ev->pos() - press_pos).manhattanLength() < QApplication::startDragDistance())
        {
            return QLabel::mouseMoveEvent(ev); // 还没到这时候
        }
        moved = true;
        move(this->pos() + ev->pos() - press_pos);
        ev->accept();
        return ;
    }
    QLabel::mouseMoveEvent(ev);
}

void CaptchaMovableLabel::mouseReleaseEvent(QMouseEvent *ev)
{
    if (dragging)
    {
        // 结束拖拽
        dragging = false;
        movingTimer.start();

        startPressAnimation(0);
    }
    if (moved)
        return ev->accept();
    QLabel::mouseReleaseEvent(ev);
}

void CaptchaMovableLabel::startPressAnimation(int end)
{
    QPropertyAnimation* ani = new QPropertyAnimation(this, "pressProgress");
    ani->setStartValue(pressProgress);
    ani->setEndValue(end);
    ani->setDuration(CAPTCHA_REFRESH_DURATION << 1);
    ani->start();
    connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
}

void CaptchaMovableLabel::setRefreshProgress(int g)
{
    this->refreshProgress = g;
    update();
}

int CaptchaMovableLabel::getRefreshProgress()
{
    return refreshProgress;
}

bool CaptchaMovableLabel::isNoAni()
{
    return refreshProgress == 100;
}

void CaptchaMovableLabel::setPressProgress(int g)
{
    this->pressProgress = g;
    double off = g / 100;
    effect.setBlurRadius(g / 20.0);
    effect.setOffset(-off, off);
}

int CaptchaMovableLabel::getPressProgress()
{
    return pressProgress;
}

void CaptchaMovableLabel::slotMovePos()
{
    if (refreshProgress < 100)
        return ;

    int val = color.red() + moveR;
    if ( val > 255)
    {
        val = 255;
        moveR = - qrand() % 5;
    }
    else if (val < 0)
    {
        val = 0;
        moveR = - qrand() % 5;
    }
    color.setRed(val);

    val = color.green() + moveG;
    if ( val > 255)
    {
        val = 255;
        moveG = - qrand() % 5;
    }
    else if (val < 0)
    {
        val = 0;
        moveG = - qrand() % 5;
    }
    color.setGreen(val);

    val = color.blue() + moveB;
    if ( val > 255)
    {
        val = 255;
        moveB = - qrand() % 5;
    }
    else if (val < 0)
    {
        val = 0;
        moveB = - qrand() % 5;
    }
    color.setBlue(val);
}

captchalabel.h

#ifndef CAPTCHALABEL_H
#define CAPTCHALABEL_H

#include "captchamovablelabel.h"
#include <QTimer>

#define CAPTCHAR_COUNT 4 // 验证码字符数量

class CaptchaLabel : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(int refreshProgress READ getRefreshProgress WRITE setRefreshProgress)
public:
    CaptchaLabel(QWidget* parent = nullptr);

    void refresh();
    void refresh_xiao();
    bool match(QString input);

private:
    void initView();
    void initData();
    void setRefreshProgress(int g);
    int getRefreshProgress();
    bool isNoAni();

private slots:
    void moveNoiseLines();

protected:
    void paintEvent(QPaintEvent* ) override;
    void mouseReleaseEvent(QMouseEvent *event) override;

private:
    CaptchaMovableLabel* charLabels[CAPTCHAR_COUNT]; // Label控件
    QList<QPoint> noisePoints; // 噪音点
    QList<QColor> pointColors; // 点的颜色

    QList<QPointF> lineStarts; // 噪音线起始点
    QList<QPointF> lineEnds; // 噪音先结束点
    QList<QPointF> startsV; // 起始点的移动速度(带方向)
    QList<QPointF> endsV; // 结束点的速度(带方向)
    QList<QColor> lineColor1s; // 线的渐变色1
    QList<QColor> lineColor2s; // 线的渐变色2
    QList<int> lineWidths;
    QTimer movingTimer;

    int refreshProgress = 100;
    QList<QPoint> noisePoints2; // 新的位置

    int autoRefreshMax = 2; // match错误几次后就自动刷新
    int matchFailCount = 0; // match错误次数
    int matchSuccessCount = 0; // match成功次数
    int matchFailAndRefreshCount = 0; // 失败且导致刷新的次数,强行刷新
};

#endif // CAPTCHALABEL_H

captchalabel.cpp

#include "captchalabel.h"

CaptchaLabel::CaptchaLabel(QWidget *parent) : QWidget(parent)
{
    initView();
    // 这里延迟,等待布局结束
    QTimer::singleShot(0, [=]{
        initData();
        refresh();
    });
}

void CaptchaLabel::initView()
{
    // 初始化控件
    for (int i = 0; i < CAPTCHAR_COUNT; i++)
    {
        charLabels[i] = new CaptchaMovableLabel(this);
        charLabels[i]->move(0, 0);
    }

    // 初始化时钟
    movingTimer.setInterval(30);
    movingTimer.setSingleShot(false);
    movingTimer.start();
    connect(&movingTimer, SIGNAL(timeout()), this, SLOT(moveNoiseLines()));
}

void CaptchaLabel::initData()
{
    // 初始化噪音线
    auto getRandomColor = [=]{
        return QColor(qrand() % 255, qrand() % 255, qrand() % 255);
    };
    int w = width(), h = height();
    int count = 20/*w * h / 400*/;
    int penW = qMin(w, h) / 15;
    for (int i = 0; i < count; i++)
    {
        lineStarts.append(QPointF(qrand() % w, qrand() % h));
        lineEnds.append(QPointF(qrand() % w, qrand() % h));
        startsV.append(QPointF((qrand() % 30 - 15) / 10.0, (qrand() % 30 - 15) / 10.0));
        endsV.append(QPointF((qrand() % 30 - 15) / 10.0, (qrand() % 30 - 15) / 10.0));
        lineWidths.append(qrand() % penW + 1);
        lineColor1s.append(getRandomColor());
        lineColor2s.append(getRandomColor());
    }
}

void CaptchaLabel::setRefreshProgress(int g)
{
    this->refreshProgress = g;
    update();
}

int CaptchaLabel::getRefreshProgress()
{
    return refreshProgress;
}

bool CaptchaLabel::isNoAni()
{
    return refreshProgress == 100;
}

void CaptchaLabel::moveNoiseLines()
{
    int w = width(), h = height();
    double vBase = 100.0; // 大概最快要3秒钟走完

    for (int i = 0; i < lineStarts.size(); i++)
    {
        QPointF& pos = lineStarts[i];
        pos += startsV.at(i);
        if (pos.x() < 0)
            startsV[i].setX(qrand() % w / vBase);
        else if (pos.x() > w)
            startsV[i].setX(- qrand() % w / vBase);
        if (pos.y() < 0)
            startsV[i].setY(qrand() % h / vBase);
        else if (pos.y() > h)
            startsV[i].setY(- qrand() % h / vBase);
    }


    for (int i = 0; i < lineEnds.size(); i++)
    {
        QPointF& pos = lineEnds[i];
        pos += endsV.at(i);
        if (pos.x() < 0)
            endsV[i].setX(qrand() % w / vBase);
        else if (pos.x() > w)
            endsV[i].setX(- qrand() % w / vBase);
        if (pos.y() < 0)
            endsV[i].setY(qrand() % h / vBase);
        else if (pos.y() > h)
            endsV[i].setY(- qrand() % h / vBase);
    }

    update();
}
void CaptchaLabel::refresh()
{
    int width = this->width();
    int height = this->height();
    // 清空全部内容
    for (int i = 0; i < CAPTCHAR_COUNT; i++)
        charLabels[i]->hide();

    refreshProgress = -1;
    update();

    // 获取背景底色
    QPixmap rend(this->size());
    render(&rend);
    QColor bgColor = rend.toImage().pixelColor(width/2, height/2);
    int br = bgColor.red(), bg = bgColor.green(), bb = bgColor.blue();

    // 开始随机生成
    const int border = 10;
    int leftest = width / border;
    int topest = height / border;
    int wid = width - leftest * 2;
    int hei = height - topest * 2;
    for (int i = 0; i < CAPTCHAR_COUNT; i++)
    {
        auto label = charLabels[i];

        // 随机大小
        QFont font;
        font.setPointSize( qrand() % 8 + 33 );
        label->setFont(font);

        // 随机旋转
        label->setAngle( qrand() % (CAPTCHA_CHAR_ANGLE_MAX*2) - CAPTCHA_CHAR_ANGLE_MAX);

        // 生成随机字符
        const QString pool = "QWERTYUIOPASDFGHJKLZXCVBNM";
        QChar rc = pool.at(qrand() % pool.size());
        // 此时会调整大小,setText必须在setFont之后
        label->setText(rc);

        // 生成随机位置(排除边缘)
        int left = leftest + wid * i / CAPTCHAR_COUNT;
        int right = leftest + wid * (i+1) / CAPTCHAR_COUNT - label->width();
        int x = qrand() % qMax(right-left, 1) + left;
        int y = qrand() % qMax(hei - label->height(), 1) + topest;
        label->show(); // 之前是hide状态
        QPropertyAnimation * ani = new QPropertyAnimation(label, "pos");
        ani->setStartValue(label->pos());
        ani->setEndValue(QPoint(x, y));
        ani->setDuration(qrand() % (CAPTCHA_REFRESH_DURATION/2) + CAPTCHA_REFRESH_DURATION/2);
        ani->setEasingCurve(QEasingCurve::OutQuart);
        ani->start();
        connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));

        // 生成随机颜色,且必须和背景颜色有区分度
        QColor color;
        while (true)
        {
            int r = qrand() % 255;
            int g = qrand() % 255;
            int b = qrand() % 255;
            if (abs(r-br) + abs(g-bg) + abs(b-bb) > 383)
            {
                color = QColor(r, g, b);
                break;
            }
        }
        label->setColor(color);

        label->startRefreshAnimation();
    }

    // 生成噪音点
    int count = wid * hei / border; // 点的数量
    if (noisePoints.size() == 0) // 第一次
    {
        for (int i = 0; i < count; i++)
        {
            int x = qrand() % width;
            int y = qrand() % height;
            noisePoints.append(QPoint(x, y / 2));
            noisePoints2.append(QPoint(x, y));
            pointColors.append(QColor(qrand() % 255, qrand() % 255, qrand() % 255));
        }
    }
    else
    {
        noisePoints = noisePoints2;
        count = noisePoints.size();

        noisePoints2.clear();
        for (int i = 0; i < count; i++)
        {
            noisePoints2.append(QPoint(qrand() % width, qrand() % height));
        }
    }

    // 生成噪音线

    QPropertyAnimation* ani = new QPropertyAnimation(this, "refreshProgress");
    ani->setStartValue(0);
    ani->setEndValue(100);
    ani->setDuration(qrand() % (CAPTCHA_REFRESH_DURATION) + CAPTCHA_REFRESH_DURATION);
    ani->start();
    connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
}

void CaptchaLabel::refresh_xiao()
{
    int width = this->width();
    int height = this->height();
    // 清空全部内容
    for (int i = 0; i < CAPTCHAR_COUNT; i++)
        charLabels[i]->hide();

    refreshProgress = -1;
    update();

    // 获取背景底色
    QPixmap rend(this->size());
    render(&rend);
    QColor bgColor = rend.toImage().pixelColor(width/2, height/2);
    int br = bgColor.red(), bg = bgColor.green(), bb = bgColor.blue();

    // 开始随机生成
    const int border = 10;
    int leftest = width / border;
    int topest = height / border;
    int wid = width - leftest * 2;
    int hei = height - topest * 2;
    for (int i = 0; i < CAPTCHAR_COUNT; i++)
    {
        auto label = charLabels[i];

        // 随机大小
        QFont font;
        font.setPointSize( qrand() % 8 + 22 );
        label->setFont(font);

        // 随机旋转
        label->setAngle( qrand() % (CAPTCHA_CHAR_ANGLE_MAX*2) - CAPTCHA_CHAR_ANGLE_MAX);

//        // 生成随机字符
//        const QString pool = "QWERTYUIOPASDFGHJKLZXCVBNM";
//        QChar rc = pool.at(qrand() % pool.size());
//        // 此时会调整大小,setText必须在setFont之后
//        label->setText(rc);

        if(i == 0)
        {
        label->setText("X");
        }
        else if(i == 1)
        {
        label->setText("I");
        }
        else if(i == 2)
        {
        label->setText("A");
        }
        else if(i == 3)
        {
        label->setText("O");
        }

        // 生成随机位置(排除边缘)
        int left = leftest + wid * i / CAPTCHAR_COUNT;
        int right = leftest + wid * (i+1) / CAPTCHAR_COUNT - label->width();
        int x = qrand() % qMax(right-left, 1) + left;
        int y = qrand() % qMax(hei - label->height(), 1) + topest;
        label->show(); // 之前是hide状态
        QPropertyAnimation * ani = new QPropertyAnimation(label, "pos");
        ani->setStartValue(label->pos());
        ani->setEndValue(QPoint(x, y));
        ani->setDuration(qrand() % (CAPTCHA_REFRESH_DURATION/2) + CAPTCHA_REFRESH_DURATION/2);
        ani->setEasingCurve(QEasingCurve::OutQuart);
        ani->start();
        connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));

        // 生成随机颜色,且必须和背景颜色有区分度
        QColor color;
        while (true)
        {
            int r = qrand() % 255;
            int g = qrand() % 255;
            int b = qrand() % 255;
            if (abs(r-br) + abs(g-bg) + abs(b-bb) > 383)
            {
                color = QColor(r, g, b);
                break;
            }
        }
        label->setColor(color);

        label->startRefreshAnimation();
    }

    // 生成噪音点
    int count = wid * hei / border; // 点的数量
    if (noisePoints.size() == 0) // 第一次
    {
        for (int i = 0; i < count; i++)
        {
            int x = qrand() % width;
            int y = qrand() % height;
            noisePoints.append(QPoint(x, y / 2));
            noisePoints2.append(QPoint(x, y));
            pointColors.append(QColor(qrand() % 255, qrand() % 255, qrand() % 255));
        }
    }
    else
    {
        noisePoints = noisePoints2;
        count = noisePoints.size();

        noisePoints2.clear();
        for (int i = 0; i < count; i++)
        {
            noisePoints2.append(QPoint(qrand() % width, qrand() % height));
        }
    }

    // 生成噪音线

    QPropertyAnimation* ani = new QPropertyAnimation(this, "refreshProgress");
    ani->setStartValue(0);
    ani->setEndValue(100);
    ani->setDuration(qrand() % (CAPTCHA_REFRESH_DURATION) + CAPTCHA_REFRESH_DURATION);
    ani->start();
    connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));
}

/**
 * 判断能否匹配
 */
bool CaptchaLabel::match(QString input)
{
    // 根据label的位置排序
    std::sort(charLabels, charLabels+CAPTCHAR_COUNT, [=](QLabel* a, QLabel* b){
        if (a->pos().x() == b->pos().x())
            return a->pos().y() < b->pos().y();
        return a->pos().x() < b->pos().x();
    });
    // 按顺序组合成新的字符串
    QString captcha;
    for (int i = 0; i < CAPTCHAR_COUNT; i++)
        captcha += charLabels[i]->text();
    // 进行比较
    if (input.toUpper() == captcha)
    {
        refresh();

        matchSuccessCount++;
        if(matchSuccessCount >= 9)
        {
          refresh_xiao();
        }

        return true;
    }
    // 记录失败
    matchFailCount++;
    if (matchFailCount >= autoRefreshMax  // 达到刷新的次数
            || matchFailAndRefreshCount > 2) // 多次错误导致刷新
    {
        refresh();
        matchSuccessCount = 0;
        matchFailAndRefreshCount++;
        matchFailCount = 0;
    }
    return false;
}

void CaptchaLabel::paintEvent(QPaintEvent *)
{
    QPainter painter(this);

    if (refreshProgress == -1) // 不画,可能需要获取背景颜色
        return ;

    // 画噪音点
    if (isNoAni())
    {
        // 显示随机的点
        for (int i = 0; i < noisePoints2.size(); i++)
        {
            painter.setPen(pointColors.at(i));
            painter.drawPoint(noisePoints2.at(i));
        }
    }
    else
    {
        // 动画过程中的点的移动
        double newProp = refreshProgress / 100.0;
        double oldProp = 1.0 - newProp;
        int count = qMin(noisePoints.size(), noisePoints2.size());
        for (int i = 0; i < count; i++)
        {
            QPoint pt1 = noisePoints.at(i);
            QPoint pt2 = noisePoints2.at(i);
            QPoint pt( pt1.x() * oldProp + pt2.x() * newProp,
                       pt1.y() * oldProp + pt2.y() * newProp );
            painter.setPen(pointColors.at(i));
            painter.drawPoint(pt);
        }
    }

    // 画噪音线
    painter.setRenderHint(QPainter::Antialiasing);
    for (int i = 0; i < lineStarts.size(); i++)
    {
        QLinearGradient grad(lineStarts.at(i), lineEnds.at(i));
        grad.setColorAt(0, lineColor1s.at(i));
        grad.setColorAt(1, lineColor2s.at(i));
        painter.setPen(QPen(grad, lineWidths.at(i)));
        painter.drawLine(lineStarts.at(i), lineEnds.at(i));
    }
}

void CaptchaLabel::mouseReleaseEvent(QMouseEvent *event)
{
    if (QRect(0,0,width(),height()).contains(event->pos()))
        refresh();
    QWidget::mouseReleaseEvent(event);
}

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include "captchalabel.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    num = 0;
    choose = 0;
}

MainWindow::~MainWindow()
{
    delete ui;
}


void MainWindow::on_pushButton_clicked()
{
    if(ui->widget->match(ui->lineEdit->text()))
    {
        QMessageBox::information(0,QObject::tr("123"),QObject::tr("验证成功"));
        ui->lineEdit->clear();

        return;
    }
}

参考:https://blog.csdn.net/wzz953200463/article/details/143028744

;