源代码续:
{% extends 'base.html' %}
{% block title %}订单详情 | {{ super() }}{% endblock %}
{% block styles %}
{{ super() }}
<style>
.order-card {
border-radius: 8px;
overflow: hidden;
margin-bottom: 30px;
}
.order-header {
background-color: #f8f9fa;
padding: 15px;
border-bottom: 1px solid #dee2e6;
}
.status-badge {
padding: 5px 10px;
border-radius: 20px;
font-size: 0.8rem;
}
.status-pending {
background-color: #ffc107;
color: #212529;
}
.status-paid {
background-color: #17a2b8;
color: white;
}
.status-completed {
background-color: #28a745;
color: white;
}
.status-cancelled {
background-color: #6c757d;
color: white;
}
.item-details {
display: flex;
padding: 15px;
border-bottom: 1px solid #dee2e6;
}
.item-image {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.item-info {
flex-grow: 1;
}
.item-price {
font-size: 1.5rem;
font-weight: bold;
color: #ff4136;
}
.order-info {
padding: 15px;
}
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.order-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
.message-box {
background-color: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
}
.review-card {
margin-top: 30px;
border-radius: 8px;
overflow: hidden;
}
.review-header {
background-color: #f8f9fa;
padding: 15px;
border-bottom: 1px solid #dee2e6;
}
.review-content {
padding: 15px;
}
.star-rating {
color: #ffc107;
font-size: 1.2rem;
}
.timeline {
margin-top: 30px;
position: relative;
padding-left: 30px;
}
.timeline::before {
content: '';
position: absolute;
left: 15px;
top: 0;
bottom: 0;
width: 2px;
background-color: #dee2e6;
}
.timeline-item {
position: relative;
margin-bottom: 20px;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-item::before {
content: '';
position: absolute;
left: -30px;
top: 5px;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #0d6efd;
}
.timeline-date {
font-size: 0.8rem;
color: #6c757d;
margin-bottom: 5px;
}
</style>
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="mb-4">订单详情</h1>
<!-- 订单卡片 -->
<div class="card order-card">
<div class="order-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">订单号:{{ order.order_number }}</h5>
<small class="text-muted">创建时间:{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<span class="status-badge status-{{ order.status }}">
{{ {'pending': '待付款', 'paid': '待完成', 'completed': '已完成', 'cancelled': '已取消'}[order.status] }}
</span>
</div>
<!-- 商品信息 -->
<div class="item-details">
<img src="{{ item.get_main_image_url() }}" class="item-image" alt="{{ item.title }}">
<div class="item-info">
<h5>{{ item.title }}</h5>
<p class="text-muted mb-1">
状况:{{ {'brand_new': '全新', 'like_new': '几乎全新', 'slightly_used': '轻微使用痕迹', 'used': '使用过', 'heavily_used': '重度使用'}[item.condition] }}
</p>
<p class="item-price">¥{{ "%.2f"|format(order.price) }}</p>
</div>
</div>
<!-- 订单信息 -->
<div class="order-info">
<h5 class="mb-3">订单信息</h5>
<div class="info-row">
<span>买家:</span>
<span>{{ order.buyer.username }}</span>
</div>
<div class="info-row">
<span>卖家:</span>
<span>{{ order.seller.username }}</span>
</div>
<div class="info-row">
<span>交易地点:</span>
<span>{{ item.location }}</span>
</div>
<div class="info-row">
<span>订单状态:</span>
<span>{{ {'pending': '待付款', 'paid': '待完成', 'completed': '已完成', 'cancelled': '已取消'}[order.status] }}</span>
</div>
<div class="info-row">
<span>最后更新:</span>
<span>{{ order.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</div>
{% if order.message %}
<div class="message-box">
<h6>买家留言:</h6>
<p class="mb-0">{{ order.message }}</p>
</div>
{% endif %}
<!-- 订单操作 -->
<div class="order-actions">
{% if is_buyer %}
{% if order.status == 'pending' %}
<form action="{{ url_for('order.pay_order', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-primary">立即付款</button>
</form>
{% elif order.status == 'paid' %}
<form action="{{ url_for('order.complete_order', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-success">确认收货</button>
</form>
{% endif %}
{% if order.status in ['pending', 'paid'] %}
<form action="{{ url_for('order.cancel_order', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-danger">取消订单</button>
</form>
{% endif %}
{% if order.status == 'completed' and not order.is_reviewed %}
<a href="{{ url_for('order.review', order_number=order.order_number) }}" class="btn btn-outline-success">评价</a>
{% endif %}
{% else %}
{% if order.status in ['pending', 'paid'] %}
<form action="{{ url_for('order.cancel_order', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-danger">取消订单</button>
</form>
{% endif %}
{% endif %}
<a href="{{ url_for('order.list_orders') }}" class="btn btn-outline-secondary">返回订单列表</a>
</div>
</div>
</div>
<!-- 评价信息 -->
{% if review %}
<div class="card review-card">
<div class="review-header">
<h5 class="mb-0">订单评价</h5>
</div>
<div class="review-content">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<img src="{{ url_for('static', filename='uploads/avatars/' + review.reviewer.avatar) if review.reviewer.avatar else url_for('static', filename='images/default_avatar.jpg') }}"
alt="{{ review.reviewer.username }}"
class="rounded-circle me-2"
style="width: 40px; height: 40px; object-fit: cover;">
<span>{{ review.reviewer.username }}</span>
</div>
<div class="star-rating">
{% for i in range(5) %}
{% if i < review.rating %}
<i class="bi bi-star-fill"></i>
{% else %}
<i class="bi bi-star"></i>
{% endif %}
{% endfor %}
</div>
</div>
<p>{{ review.content }}</p>
<small class="text-muted">{{ review.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
</div>
{% endif %}
<!-- 订单时间线 -->
<div class="timeline">
<h5 class="mb-3">订单进度</h5>
<div class="timeline-item">
<div class="timeline-date">{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
<div class="timeline-content">
<strong>订单创建</strong>
<p>买家 {{ order.buyer.username }} 创建了订单</p>
</div>
</div>
{% if order.status != 'pending' %}
<div class="timeline-item">
<div class="timeline-date">{{ order.updated_at.strftime('%Y-%m-%d %H:%M:%S') if order.status == 'paid' else '' }}</div>
<div class="timeline-content">
<strong>订单支付</strong>
<p>买家已支付订单</p>
</div>
</div>
{% endif %}
{% if order.status == 'completed' %}
<div class="timeline-item">
<div class="timeline-date">{{ order.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
<div class="timeline-content">
<strong>订单完成</strong>
<p>买家确认收货,交易完成</p>
</div>
</div>
{% endif %}
{% if order.status == 'cancelled' %}
<div class="timeline-item">
<div class="timeline-date">{{ order.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
<div class="timeline-content">
<strong>订单取消</strong>
<p>订单已取消</p>
</div>
</div>
{% endif %}
{% if review %}
<div class="timeline-item">
<div class="timeline-date">{{ review.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
<div class="timeline-content">
<strong>订单评价</strong>
<p>买家已评价此订单</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\order\list.html
{% extends 'base.html' %}
{% block title %}我的订单 | {{ super() }}{% endblock %}
{% block styles %}
{{ super() }}
<style>
.order-tabs {
margin-bottom: 30px;
}
.order-card {
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.order-content {
display: flex;
padding: 15px;
}
.order-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.order-details {
flex-grow: 1;
}
.order-price {
font-size: 1.2rem;
font-weight: bold;
color: #ff4136;
}
.order-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #dee2e6;
}
.status-badge {
padding: 5px 10px;
border-radius: 20px;
font-size: 0.8rem;
}
.status-pending {
background-color: #ffc107;
color: #212529;
}
.status-paid {
background-color: #17a2b8;
color: white;
}
.status-completed {
background-color: #28a745;
color: white;
}
.status-cancelled {
background-color: #6c757d;
color: white;
}
.empty-orders {
text-align: center;
padding: 50px 0;
}
</style>
{% endblock %}
{% block content %}
<div class="container my-5">
<h1 class="mb-4">我的订单</h1>
<!-- 角色切换 -->
<div class="btn-group mb-4">
<a href="{{ url_for('order.list_orders', role='buyer') }}" class="btn btn-outline-primary {{ 'active' if role == 'buyer' }}">我购买的</a>
<a href="{{ url_for('order.list_orders', role='seller') }}" class="btn btn-outline-primary {{ 'active' if role == 'seller' }}">我出售的</a>
</div>
<!-- 状态筛选 -->
<div class="order-tabs">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {{ 'active' if status == 'all' }}" href="{{ url_for('order.list_orders', role=role, status='all') }}">全部</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if status == 'pending' }}" href="{{ url_for('order.list_orders', role=role, status='pending') }}">待付款</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if status == 'paid' }}" href="{{ url_for('order.list_orders', role=role, status='paid') }}">待完成</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if status == 'completed' }}" href="{{ url_for('order.list_orders', role=role, status='completed') }}">已完成</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if status == 'cancelled' }}" href="{{ url_for('order.list_orders', role=role, status='cancelled') }}">已取消</a>
</li>
</ul>
</div>
<!-- 订单列表 -->
{% if orders %}
{% for order in orders %}
<div class="card order-card">
<div class="order-header">
<div>
<span class="text-muted">订单号:{{ order.order_number }}</span>
<span class="text-muted ms-3">{{ order.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
<div>
<span class="status-badge status-{{ order.status }}">
{{ {'pending': '待付款', 'paid': '待完成', 'completed': '已完成', 'cancelled': '已取消'}[order.status] }}
</span>
</div>
</div>
<div class="order-content">
{% set item = order.item %}
<img src="{{ item.get_main_image_url() }}" class="order-image" alt="{{ item.title }}">
<div class="order-details">
<h5>{{ item.title }}</h5>
<div class="d-flex justify-content-between mb-2">
<div>
{% if role == 'buyer' %}
<p class="text-muted mb-1">卖家:{{ order.seller.username }}</p>
{% else %}
<p class="text-muted mb-1">买家:{{ order.buyer.username }}</p>
{% endif %}
<p class="text-muted mb-1">
状况:{{ {'brand_new': '全新', 'like_new': '几乎全新', 'slightly_used': '轻微使用痕迹', 'used': '使用过', 'heavily_used': '重度使用'}[item.condition] }}
</p>
</div>
<div class="text-end">
<p class="order-price mb-0">¥{{ "%.2f"|format(order.price) }}</p>
</div>
</div>
<div class="order-actions">
<a href="{{ url_for('order.detail', order_number=order.order_number) }}" class="btn btn-outline-primary">查看详情</a>
{% if role == 'buyer' %}
{% if order.status == 'pending' %}
<form action="{{ url_for('order.pay_order', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-primary">立即付款</button>
</form>
{% elif order.status == 'paid' %}
<form action="{{ url_for('order.complete_order', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-success">确认收货</button>
</form>
{% endif %}
{% if order.status in ['pending', 'paid'] %}
<form action="{{ url_for('order.cancel_order', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-danger">取消订单</button>
</form>
{% endif %}
{% if order.status == 'completed' and not order.is_reviewed %}
<a href="{{ url_for('order.review', order_number=order.order_number) }}" class="btn btn-outline-success">评价</a>
{% endif %}
{% else %}
{% if order.status in ['pending', 'paid'] %}
<form action="{{ url_for('order.cancel_order', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-danger">取消订单</button>
</form>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-orders">
<img src="{{ url_for('static', filename='images/empty_orders.svg') }}" alt="暂无订单" style="max-width: 200px; margin-bottom: 20px;">
<h3>暂无订单</h3>
{% if role == 'buyer' %}
<p class="text-muted">您还没有购买过商品</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary mt-3">去逛逛</a>
{% else %}
<p class="text-muted">您还没有出售过商品</p>
<a href="{{ url_for('item.new_item') }}" class="btn btn-primary mt-3">发布商品</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
app\templates\order\review.html
{% extends 'base.html' %}
{% block title %}评价订单 | {{ super() }}{% endblock %}
{% block styles %}
{{ super() }}
<style>
.review-card {
border-radius: 8px;
overflow: hidden;
}
.item-summary {
display: flex;
margin-bottom: 20px;
padding: 15px;
border: 1px solid #dee2e6;
border-radius: 8px;
background-color: #f8f9fa;
}
.item-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.item-details {
flex-grow: 1;
}
.item-price {
font-size: 1.2rem;
font-weight: bold;
color: #ff4136;
}
.star-rating {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
}
.star-rating input {
display: none;
}
.star-rating label {
cursor: pointer;
width: 40px;
height: 40px;
margin-right: 5px;
position: relative;
font-size: 30px;
color: #ddd;
}
.star-rating label:before {
content: '★';
position: absolute;
opacity: 0;
color: #ffc107;
}
.star-rating label:hover:before,
.star-rating label:hover ~ label:before,
.star-rating input:checked ~ label:before {
opacity: 1;
}
.rating-text {
margin-top: 10px;
font-weight: bold;
min-height: 24px;
}
</style>
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="mb-4">评价订单</h1>
<!-- 商品摘要 -->
<div class="item-summary">
<img src="{{ item.get_main_image_url() }}" class="item-image" alt="{{ item.title }}">
<div class="item-details">
<h5>{{ item.title }}</h5>
<p class="text-muted mb-1">卖家:{{ item.seller.username }}</p>
<p class="text-muted mb-1">
状况:{{ {'brand_new': '全新', 'like_new': '几乎全新', 'slightly_used': '轻微使用痕迹', 'used': '使用过', 'heavily_used': '重度使用'}[item.condition] }}
</p>
<p class="item-price mb-0">¥{{ "%.2f"|format(item.price) }}</p>
</div>
</div>
<!-- 评价表单 -->
<div class="card review-card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">订单评价</h4>
</div>
<div class="card-body">
<form method="post">
{{ form.hidden_tag() }}
<div class="mb-4">
<label class="form-label">评分</label>
<div class="star-rating">
<input type="radio" id="star5" name="rating" value="5" {{ 'checked' if form.rating.data == 5 }}>
<label for="star5" title="5星"></label>
<input type="radio" id="star4" name="rating" value="4" {{ 'checked' if form.rating.data == 4 }}>
<label for="star4" title="4星"></label>
<input type="radio" id="star3" name="rating" value="3" {{ 'checked' if form.rating.data == 3 }}>
<label for="star3" title="3星"></label>
<input type="radio" id="star2" name="rating" value="2" {{ 'checked' if form.rating.data == 2 }}>
<label for="star2" title="2星"></label>
<input type="radio" id="star1" name="rating" value="1" {{ 'checked' if form.rating.data == 1 }}>
<label for="star1" title="1星"></label>
</div>
<div class="rating-text" id="ratingText">
{% if form.rating.data %}
{{ ['很差', '较差', '一般', '不错', '很好'][form.rating.data-1] }}
{% endif %}
</div>
{% if form.rating.errors %}
<div class="invalid-feedback d-block">
{% for error in form.rating.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.content.label(class="form-label") }}
{{ form.content(class="form-control", rows=5, placeholder="请分享您的购物体验,对其他买家有所帮助") }}
{% if form.content.errors %}
<div class="invalid-feedback d-block">
{% for error in form.content.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="alert alert-info">
<h5 class="alert-heading"><i class="bi bi-info-circle"></i> 评价提示</h5>
<ul class="mb-0">
<li>评价一旦提交将无法修改</li>
<li>请客观公正地评价交易体验</li>
<li>您的评价将帮助其他买家做出更好的决策</li>
</ul>
</div>
<div class="d-grid gap-2 mt-4">
{{ form.submit(class="btn btn-primary btn-lg") }}
<a href="{{ url_for('order.detail', order_number=order.order_number) }}" class="btn btn-outline-secondary">取消</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// 星级评分交互
const ratingInputs = document.querySelectorAll('.star-rating input');
const ratingText = document.getElementById('ratingText');
const ratingTexts = ['很差', '较差', '一般', '不错', '很好'];
ratingInputs.forEach(input => {
input.addEventListener('change', function() {
const rating = parseInt(this.value);
ratingText.textContent = ratingTexts[rating - 1];
});
});
</script>
{% endblock %}
app\templates\transaction\dispute.html
{% extends 'base.html' %}
{% block title %}提交纠纷 | {{ super() }}{% endblock %}
{% block styles %}
{{ super() }}
<style>
.dispute-card {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.dispute-header {
background-color: #f8f9fa;
padding: 20px;
border-bottom: 1px solid #dee2e6;
}
.item-summary {
display: flex;
padding: 15px;
border-bottom: 1px solid #dee2e6;
}
.item-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.item-details {
flex-grow: 1;
}
.item-price {
font-size: 1.2rem;
font-weight: bold;
color: #ff4136;
}
.dispute-guidelines {
margin-top: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.image-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.preview-item {
position: relative;
width: 100px;
height: 100px;
border-radius: 4px;
overflow: hidden;
}
.preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-item .remove-btn {
position: absolute;
top: 5px;
right: 5px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
}
.upload-placeholder {
width: 100px;
height: 100px;
border: 2px dashed #dee2e6;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.upload-placeholder i {
font-size: 24px;
color: #6c757d;
}
</style>
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="mb-4">提交纠纷申请</h1>
<div class="card dispute-card">
<!-- 纠纷头部 -->
<div class="dispute-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">订单号:{{ order.order_number }}</h5>
<span class="badge bg-{{ 'info' if order.status == 'paid' else 'success' }}">
{{ '待完成' if order.status == 'paid' else '已完成' }}
</span>
</div>
<small class="text-muted">创建时间:{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<!-- 商品摘要 -->
<div class="item-summary">
<img src="{{ item.get_main_image_url() }}" class="item-image" alt="{{ item.title }}">
<div class="item-details">
<h5>{{ item.title }}</h5>
<p class="text-muted mb-1">
{% if current_user.id == order.buyer_id %}
卖家:{{ order.seller.username }}
{% else %}
买家:{{ order.buyer.username }}
{% endif %}
</p>
<p class="text-muted mb-1">
状况:{{ {'brand_new': '全新', 'like_new': '几乎全新', 'slightly_used': '轻微使用痕迹', 'used': '使用过', 'heavily_used': '重度使用'}[item.condition] }}
</p>
<p class="item-price mb-0">¥{{ "%.2f"|format(order.price) }}</p>
</div>
</div>
<!-- 纠纷表单 -->
<div class="p-4">
<form method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.reason.label(class="form-label") }}
{{ form.reason(class="form-select") }}
{% if form.reason.errors %}
<div class="invalid-feedback d-block">
{% for error in form.reason.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label") }}
{{ form.description(class="form-control", rows=5, placeholder="请详细描述您遇到的问题,以便我们更好地帮助您解决") }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">
{% for error in form.description.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">请提供尽可能详细的问题描述,这将有助于更快地解决纠纷</div>
</div>
<div class="mb-3">
{{ form.evidence_images.label(class="form-label") }}
<div class="d-flex align-items-center">
{{ form.evidence_images(class="form-control d-none", id="evidence_images") }}
<div class="image-preview" id="imagePreview">
<div class="upload-placeholder" id="uploadPlaceholder">
<i class="bi bi-plus"></i>
</div>
</div>
</div>
{% if form.evidence_images.errors %}
<div class="invalid-feedback d-block">
{% for error in form.evidence_images.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">上传相关证据图片,如商品实物照片、聊天记录截图等</div>
</div>
<div class="mb-3">
{{ form.preferred_solution.label(class="form-label") }}
{{ form.preferred_solution(class="form-select") }}
{% if form.preferred_solution.errors %}
<div class="invalid-feedback d-block">
{% for error in form.preferred_solution.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.contact_phone.label(class="form-label") }}
{{ form.contact_phone(class="form-control", placeholder="请输入您的联系电话") }}
{% if form.contact_phone.errors %}
<div class="invalid-feedback d-block">
{% for error in form.contact_phone.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">请确保电话畅通,以便客服人员与您联系</div>
</div>
<div class="alert alert-warning">
<h5 class="alert-heading"><i class="bi bi-exclamation-triangle"></i> 注意事项</h5>
<p>提交纠纷申请后,平台将介入处理,请保持联系方式畅通。恶意提交纠纷可能会影响您的账户信用。</p>
</div>
<div class="d-grid gap-2 mt-4">
{{ form.submit(class="btn btn-primary btn-lg") }}
<a href="{{ url_for('order.detail', order_number=order.order_number) }}" class="btn btn-outline-secondary">取消</a>
</div>
</form>
</div>
</div>
<!-- 纠纷指南 -->
<div class="dispute-guidelines">
<h5><i class="bi bi-info-circle"></i> 纠纷处理指南</h5>
<ol class="mb-0">
<li>提交纠纷申请后,平台将在24小时内进行初步审核</li>
<li>审核通过后,客服人员将与双方联系,了解情况并协调解决</li>
<li>如双方无法达成一致,平台将根据提供的证据和交易记录做出裁决</li>
<li>纠纷处理期间,相关订单资金将被冻结</li>
<li>纠纷解决后,平台将根据结果处理订单和资金</li>
</ol>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const imageInput = document.getElementById('evidence_images');
const imagePreview = document.getElementById('imagePreview');
const uploadPlaceholder = document.getElementById('uploadPlaceholder');
// 最大上传图片数量
const maxImages = 3;
// 已上传图片数量
let uploadedCount = 0;
// 点击上传占位符触发文件选择
uploadPlaceholder.addEventListener('click', function() {
if (uploadedCount < maxImages) {
imageInput.click();
}
});
// 处理文件选择
imageInput.addEventListener('change', function() {
if (this.files && this.files.length > 0) {
// 检查是否超过最大数量
if (uploadedCount + this.files.length > maxImages) {
alert(`最多只能上传${maxImages}张图片`);
return;
}
// 处理每个选择的文件
for (let i = 0; i < this.files.length; i++) {
const file = this.files[i];
// 检查文件类型
if (!file.type.match('image/jpeg') && !file.type.match('image/png')) {
alert('只能上传JPG或PNG格式的图片');
continue;
}
// 创建预览元素
const previewItem = document.createElement('div');
previewItem.className = 'preview-item';
// 创建图片元素
const img = document.createElement('img');
img.file = file;
previewItem.appendChild(img);
// 创建删除按钮
const removeBtn = document.createElement('div');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '<i class="bi bi-x"></i>';
removeBtn.addEventListener('click', function(e) {
e.stopPropagation();
previewItem.remove();
uploadedCount--;
// 如果删除后数量小于最大值,显示上传占位符
if (uploadedCount < maxImages && !imagePreview.contains(uploadPlaceholder)) {
imagePreview.appendChild(uploadPlaceholder);
}
});
previewItem.appendChild(removeBtn);
// 插入到预览区域
imagePreview.insertBefore(previewItem, uploadPlaceholder);
// 读取文件并设置预览
const reader = new FileReader();
reader.onload = (function(aImg) {
return function(e) {
aImg.src = e.target.result;
};
})(img);
reader.readAsDataURL(file);
uploadedCount++;
}
// 如果达到最大数量,隐藏上传占位符
if (uploadedCount >= maxImages) {
uploadPlaceholder.remove();
}
}
});
});
</script>
{% endblock %}
app\templates\transaction\payment.html
{% extends 'base.html' %}
{% block title %}支付订单 | {{ super() }}{% endblock %}
{% block styles %}
{{ super() }}
<style>
.payment-card {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.payment-header {
background-color: #f8f9fa;
padding: 20px;
border-bottom: 1px solid #dee2e6;
}
.item-summary {
display: flex;
padding: 15px;
border-bottom: 1px solid #dee2e6;
}
.item-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.item-details {
flex-grow: 1;
}
.item-price {
font-size: 1.5rem;
font-weight: bold;
color: #ff4136;
}
.payment-method-option {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
cursor: pointer;
transition: all 0.3s;
}
.payment-method-option:hover {
border-color: #0d6efd;
}
.payment-method-option.selected {
border-color: #0d6efd;
background-color: rgba(13, 110, 253, 0.05);
}
.payment-method-option img {
height: 30px;
margin-right: 10px;
}
.payment-total {
background-color: #f8f9fa;
padding: 20px;
border-top: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
.payment-instructions {
margin-top: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
</style>
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="mb-4">订单支付</h1>
<div class="card payment-card">
<!-- 支付头部 -->
<div class="payment-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">订单号:{{ order.order_number }}</h5>
<span class="badge bg-warning">待支付</span>
</div>
<small class="text-muted">创建时间:{{ order.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<!-- 商品摘要 -->
<div class="item-summary">
<img src="{{ item.get_main_image_url() }}" class="item-image" alt="{{ item.title }}">
<div class="item-details">
<h5>{{ item.title }}</h5>
<p class="text-muted mb-1">卖家:{{ order.seller.username }}</p>
<p class="text-muted mb-1">
状况:{{ {'brand_new': '全新', 'like_new': '几乎全新', 'slightly_used': '轻微使用痕迹', 'used': '使用过', 'heavily_used': '重度使用'}[item.condition] }}
</p>
<p class="item-price mb-0">¥{{ "%.2f"|format(order.price) }}</p>
</div>
</div>
<!-- 支付表单 -->
<div class="p-4">
<form method="post">
{{ form.hidden_tag() }}
<h5 class="mb-3">选择支付方式</h5>
<div class="payment-methods">
{% for value, label in form.payment_method.choices %}
<div class="payment-method-option" data-value="{{ value }}">
<div class="d-flex align-items-center">
<input type="radio" name="payment_method" id="method_{{ value }}" value="{{ value }}"
{% if form.payment_method.data == value %}checked{% endif %}
class="form-check-input me-2">
<img src="{{ url_for('static', filename='images/payment/' + value + '.png') }}" alt="{{ label }}">
<label for="method_{{ value }}" class="form-check-label">{{ label }}</label>
</div>
</div>
{% endfor %}
</div>
{% if form.payment_method.errors %}
<div class="invalid-feedback d-block mb-3">
{% for error in form.payment_method.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="mt-4">
<div class="form-check">
{{ form.agreement(class="form-check-input") }}
{{ form.agreement.label(class="form-check-label") }}
</div>
{% if form.agreement.errors %}
<div class="invalid-feedback d-block">
{% for error in form.agreement.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<!-- 支付总额 -->
<div class="payment-total">
<span class="fs-5">支付总额:</span>
<span class="item-price">¥{{ "%.2f"|format(order.price) }}</span>
</div>
<div class="d-grid gap-2 mt-4">
{{ form.submit(class="btn btn-primary btn-lg") }}
<a href="{{ url_for('order.detail', order_number=order.order_number) }}" class="btn btn-outline-secondary">取消</a>
</div>
</form>
</div>
</div>
<!-- 支付说明 -->
<div class="payment-instructions">
<h5><i class="bi bi-info-circle"></i> 支付说明</h5>
<ul class="mb-0">
<li>支付成功后,请与卖家联系确认交易地点和时间</li>
<li>线下交易时请检查商品后再确认收货</li>
<li>如遇到问题,可以申请平台介入处理</li>
<li>为保障交易安全,请不要在平台外进行交易</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// 支付方式选择交互
document.addEventListener('DOMContentLoaded', function() {
const paymentOptions = document.querySelectorAll('.payment-method-option');
paymentOptions.forEach(option => {
option.addEventListener('click', function() {
// 设置单选框为选中状态
const radio = this.querySelector('input[type="radio"]');
radio.checked = true;
// 更新选中样式
paymentOptions.forEach(opt => {
opt.classList.remove('selected');
});
this.classList.add('selected');
});
// 初始化选中状态
const radio = option.querySelector('input[type="radio"]');
if (radio.checked) {
option.classList.add('selected');
}
});
});
</script>
{% endblock %}
app\templates\transaction\records.html
{% extends 'base.html' %}
{% block title %}交易记录 | {{ super() }}{% endblock %}
{% block styles %}
{{ super() }}
<style>
.transaction-filter {
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.transaction-card {
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
}
.transaction-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.transaction-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.transaction-content {
display: flex;
padding: 15px;
}
.transaction-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.transaction-details {
flex-grow: 1;
}
.transaction-price {
font-size: 1.2rem;
font-weight: bold;
color: #ff4136;
}
.transaction-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #dee2e6;
}
.status-badge {
padding: 5px 10px;
border-radius: 20px;
font-size: 0.8rem;
}
.status-pending {
background-color: #ffc107;
color: #212529;
}
.status-paid {
background-color: #17a2b8;
color: white;
}
.status-completed {
background-color: #28a745;
color: white;
}
.status-cancelled {
background-color: #6c757d;
color: white;
}
.status-disputed {
background-color: #dc3545;
color: white;
}
.empty-transactions {
text-align: center;
padding: 50px 0;
}
.pagination {
justify-content: center;
margin-top: 2rem;
}
.transaction-type {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.8rem;
margin-right: 10px;
}
.type-buy {
background-color: #e9f5ff;
color: #0d6efd;
}
.type-sell {
background-color: #e9fff0;
color: #28a745;
}
</style>
{% endblock %}
{% block content %}
<div class="container my-5">
<h1 class="mb-4">交易记录</h1>
<!-- 筛选器 -->
<div class="transaction-filter">
<form method="get" class="row g-3">
<div class="col-md-3">
<label for="role" class="form-label">交易角色</label>
<select name="role" id="role" class="form-select">
<option value="all" {% if request.args.get('role') == 'all' or not request.args.get('role') %}selected{% endif %}>全部</option>
<option value="buyer" {% if request.args.get('role') == 'buyer' %}selected{% endif %}>买家</option>
<option value="seller" {% if request.args.get('role') == 'seller' %}selected{% endif %}>卖家</option>
</select>
</div>
<div class="col-md-3">
<label for="status" class="form-label">交易状态</label>
<select name="status" id="status" class="form-select">
<option value="all" {% if request.args.get('status') == 'all' or not request.args.get('status') %}selected{% endif %}>全部</option>
<option value="pending" {% if request.args.get('status') == 'pending' %}selected{% endif %}>待付款</option>
<option value="paid" {% if request.args.get('status') == 'paid' %}selected{% endif %}>待完成</option>
<option value="completed" {% if request.args.get('status') == 'completed' %}selected{% endif %}>已完成</option>
<option value="cancelled" {% if request.args.get('status') == 'cancelled' %}selected{% endif %}>已取消</option>
<option value="disputed" {% if request.args.get('status') == 'disputed' %}selected{% endif %}>纠纷中</option>
</select>
</div>
<div class="col-md-3">
<label for="date_range" class="form-label">时间范围</label>
<select name="date_range" id="date_range" class="form-select">
<option value="all" {% if request.args.get('date_range') == 'all' or not request.args.get('date_range') %}selected{% endif %}>全部时间</option>
<option value="week" {% if request.args.get('date_range') == 'week' %}selected{% endif %}>最近一周</option>
<option value="month" {% if request.args.get('date_range') == 'month' %}selected{% endif %}>最近一个月</option>
<option value="three_months" {% if request.args.get('date_range') == 'three_months' %}selected{% endif %}>最近三个月</option>
<option value="year" {% if request.args.get('date_range') == 'year' %}selected{% endif %}>最近一年</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">筛选</button>
</div>
</form>
</div>
<!-- 交易列表 -->
{% if orders %}
{% for order in orders %}
<div class="card transaction-card">
<div class="transaction-header">
<div>
<span class="text-muted">订单号:{{ order.order_number }}</span>
<span class="transaction-type {{ 'type-buy' if order.buyer_id == current_user.id else 'type-sell' }}">
{{ '买入' if order.buyer_id == current_user.id else '卖出' }}
</span>
<span class="text-muted">{{ order.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
<div>
<span class="status-badge status-{{ order.status }}">
{{ {'pending': '待付款', 'paid': '待完成', 'completed': '已完成', 'cancelled': '已取消', 'disputed': '纠纷中'}[order.status] }}
</span>
</div>
</div>
<div class="transaction-content">
{% set item = order.item %}
<img src="{{ item.get_main_image_url() }}" class="transaction-image" alt="{{ item.title }}">
<div class="transaction-details">
<h5>{{ item.title }}</h5>
<div class="d-flex justify-content-between mb-2">
<div>
{% if order.buyer_id == current_user.id %}
<p class="text-muted mb-1">卖家:{{ order.seller.username }}</p>
{% else %}
<p class="text-muted mb-1">买家:{{ order.buyer.username }}</p>
{% endif %}
<p class="text-muted mb-1">
状况:{{ {'brand_new': '全新', 'like_new': '几乎全新', 'slightly_used': '轻微使用痕迹', 'used': '使用过', 'heavily_used': '重度使用'}[item.condition] }}
</p>
</div>
<div class="text-end">
<p class="transaction-price mb-0">¥{{ "%.2f"|format(order.price) }}</p>
</div>
</div>
<div class="transaction-actions">
<a href="{{ url_for('order.detail', order_number=order.order_number) }}" class="btn btn-outline-primary">查看详情</a>
{% if order.buyer_id == current_user.id %}
{% if order.status == 'pending' %}
<a href="{{ url_for('transaction.payment', order_number=order.order_number) }}" class="btn btn-primary">立即付款</a>
{% elif order.status == 'paid' %}
<form action="{{ url_for('transaction.complete_transaction', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-success">确认收货</button>
</form>
{% endif %}
{% if order.status in ['pending', 'paid'] %}
<form action="{{ url_for('transaction.cancel_transaction', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-danger">取消订单</button>
</form>
{% endif %}
{% if order.status in ['paid', 'completed'] and order.status != 'disputed' %}
<a href="{{ url_for('transaction.dispute', order_number=order.order_number) }}" class="btn btn-outline-warning">申请纠纷</a>
{% endif %}
{% else %}
{% if order.status in ['pending', 'paid'] %}
<form action="{{ url_for('transaction.cancel_transaction', order_number=order.order_number) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-danger">取消订单</button>
</form>
{% endif %}
{% if order.status in ['paid', 'completed'] and order.status != 'disputed' %}
<a href="{{ url_for('transaction.dispute', order_number=order.order_number) }}" class="btn btn-outline-warning">申请纠纷</a>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
<!-- 分页 -->
{% if pagination.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('transaction.transaction_records', page=pagination.prev_num, role=request.args.get('role', 'all'), status=request.args.get('status', 'all'), date_range=request.args.get('date_range', 'all')) }}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% endif %}
{% for page in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
{% if page %}
{% if page == pagination.page %}
<li class="page-item active">
<a class="page-link" href="#">{{ page }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('transaction.transaction_records', page=page, role=request.args.get('role', 'all'), status=request.args.get('status', 'all'), date_range=request.args.get('date_range', 'all')) }}">{{ page }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('transaction.transaction_records', page=pagination.next_num, role=request.args.get('role', 'all'), status=request.args.get('status', 'all'), date_range=request.args.get('date_range', 'all')) }}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="empty-transactions">
<img src="{{ url_for('static', filename='images/empty_transactions.svg') }}" alt="暂无交易记录" style="max-width: 200px; margin-bottom: 20px;">
<h3>暂无交易记录</h3>
<p class="text-muted">您还没有任何交易记录</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary mt-3">浏览商品</a>
</div>
{% endif %}
</div>
{% endblock %}
app\templates\transaction\statistics.html
{% extends 'base.html' %}
{% block title %}交易统计 | {{ super() }}{% endblock %}
{% block styles %}
{{ super() }}
<style>
.stats-card {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: transform 0.3s;
height: 100%;
}
.stats-card:hover {
transform: translateY(-5px);
}
.stats-header {
padding: 15px;
border-bottom: 1px solid #dee2e6;
}
.stats-body {
padding: 20px;
text-align: center;
}
.stats-value {
font-size: 2.5rem;
font-weight: bold;
margin: 10px 0;
color: #0d6efd;
}
.stats-label {
color: #6c757d;
font-size: 0.9rem;
}
.stats-icon {
font-size: 2rem;
margin-bottom: 15px;
}
.recent-transaction {
display: flex;
padding: 15px;
border-bottom: 1px solid #dee2e6;
}
.recent-transaction:last-child {
border-bottom: none;
}
.transaction-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.transaction-details {
flex-grow: 1;
}
.transaction-price {
font-weight: bold;
color: #ff4136;
}
.review-item {
padding: 15px;
border-bottom: 1px solid #dee2e6;
}
.review-item:last-child {
border-bottom: none;
}
.review-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.reviewer-info {
display: flex;
align-items: center;
}
.reviewer-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 10px;
object-fit: cover;
}
.star-rating {
color: #ffc107;
}
.chart-container {
height: 300px;
margin-top: 20px;
}
.rating-summary {
display: flex;
align-items: center;
margin-top: 20px;
}
.rating-circle {
width: 100px;
height: 100px;
border-radius: 50%;
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
}
.rating-value {
font-size: 2rem;
font-weight: bold;
color: #0d6efd;
}
.rating-bars {
flex-grow: 1;
}
.rating-bar {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.rating-label {
width: 50px;
}
.rating-progress {
flex-grow: 1;
height: 10px;
background-color: #e9ecef;
border-radius: 5px;
margin: 0 10px;
overflow: hidden;
}
.rating-progress-fill {
height: 100%;
background-color: #0d6efd;
}
.rating-count {
width: 30px;
text-align: right;
font-size: 0.8rem;
color: #6c757d;
}
</style>
{% endblock %}
{% block content %}
<div class="container my-5">
<h1 class="mb-4">交易统计</h1>
<!-- 统计卡片 -->
<div class="row mb-4">
<div class="col-md-3 mb-4">
<div class="stats-card">
<div class="stats-header bg-primary text-white">
<h5 class="mb-0">购买订单</h5>
</div>
<div class="stats-body">
<div class="stats-icon text-primary">
<i class="bi bi-bag"></i>
</div>
<div class="stats-value">{{ buyer_completed }}</div>
<div class="stats-label">已完成的购买订单</div>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="stats-card">
<div class="stats-header bg-success text-white">
<h5 class="mb-0">出售订单</h5>
</div>
<div class="stats-body">
<div class="stats-icon text-success">
<i class="bi bi-shop"></i>
</div>
<div class="stats-value">{{ seller_completed }}</div>
<div class="stats-label">已完成的出售订单</div>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="stats-card">
<div class="stats-header bg-info text-white">
<h5 class="mb-0">消费总额</h5>
</div>
<div class="stats-body">
<div class="stats-icon text-info">
<i class="bi bi-cash-stack"></i>
</div>
<div class="stats-value">¥{{ "%.2f"|format(buyer_total) }}</div>
<div class="stats-label">购买商品总消费</div>
</div>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="stats-card">
<div class="stats-header bg-warning text-white">
<h5 class="mb-0">收入总额</h5>
</div>
<div class="stats-body">
<div class="stats-icon text-warning">
<i class="bi bi-wallet2"></i>
</div>
<div class="stats-value">¥{{ "%.2f"|format(seller_total) }}</div>
<div class="stats-label">出售商品总收入</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- 最近交易 -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">最近交易</h5>
</div>
<div class="card-body p-0">
{% if recent_orders %}
{% for order in recent_orders %}
<div class="recent-transaction">
<img src="{{ order.item.get_main_image_url() }}" class="transaction-image" alt="{{ order.item.title }}">
<div class="transaction-details">
<h6 class="mb-1">{{ order.item.title }}</h6>
<div class="d-flex justify-content-between">
<small class="text-muted">
{% if order.buyer_id == current_user.id %}
从 {{ order.seller.username }} 购买
{% else %}
卖给 {{ order.buyer.username }}
{% endif %}
</small>
<span class="transaction-price">¥{{ "%.2f"|format(order.price) }}</span>
</div>
<small class="text-muted">{{ order.updated_at.strftime('%Y-%m-%d %H:%M') }}</small>
</div>
</div>
{% endfor %}
{% else %}
<div class="p-4 text-center">
<p class="text-muted mb-0">暂无交易记录</p>
</div>
{% endif %}
</div>
<div class="card-footer text-center">
<a href="{{ url_for('transaction.transaction_records') }}" class="btn btn-sm btn-outline-primary">查看全部交易</a>
</div>
</div>
</div>
<!-- 评价统计 -->
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">我的评价</h5>
</div>
<div class="card-body">
<!-- 评分总览 -->
<div class="rating-summary">
<div class="rating-circle">
<div class="rating-value">{{ "%.1f"|format(avg_rating) }}</div>
</div>
<div class="rating-bars">
{% for i in range(5, 0, -1) %}
<div class="rating-bar">
<div class="rating-label">{{ i }}星</div>
<div class="rating-progress">
{% set count = reviews|selectattr('rating', 'eq', i)|list|length %}
{% set percentage = (count / reviews|length * 100) if reviews|length > 0 else 0 %}
<div class="rating-progress-fill" style="width: {{ percentage }}%"></div>
</div>
<div class="rating-count">{{ count }}</div>
</div>
{% endfor %}
</div>
</div>
<!-- 最近评价 -->
<h6 class="mt-4 mb-3">最近评价</h6>
{% if reviews %}
{% for review in reviews %}
<div class="review-item">
<div class="review-header">
<div class="reviewer-info">
<img src="{{ url_for('static', filename='uploads/avatars/' + review.reviewer.avatar) if review.reviewer.avatar else url_for('static', filename='images/default_avatar.jpg') }}"
alt="{{ review.reviewer.username }}"
class="reviewer-avatar">
<span>{{ review.reviewer.username }}</span>
</div>
<div class="star-rating">
{% for i in range(5) %}
{% if i < review.rating %}
<i class="bi bi-star-fill"></i>
{% else %}
<i class="bi bi-star"></i>
{% endif %}
{% endfor %}
</div>
</div>
<p class="mb-1">{{ review.content }}</p>
<small class="text-muted">{{ review.created_at.strftime('%Y-%m-%d') }}</small>
</div>
{% endfor %}
{% else %}
<div class="text-center py-3">
<p class="text-muted mb-0">暂无评价</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 交易趋势图 -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">交易趋势</h5>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="transactionChart"></canvas>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 示例数据 - 实际应用中应从后端获取
const months = ['1月', '2月', '3月', '4月', '5月', '6月'];
const buyData = [0, 0, 0, 0, 0, 0]; // 这里应该是实际的购买数据
const sellData = [0, 0, 0, 0, 0, 0]; // 这里应该是实际的出售数据
// 生成一些随机数据用于演示
for (let i = 0; i < months.length; i++) {
buyData[i] = Math.floor(Math.random() * 5);
sellData[i] = Math.floor(Math.random() * 5);
}
// 创建图表
const ctx = document.getElementById('transactionChart').getContext('2d');
const transactionChart = new Chart(ctx, {
type: 'line',
data: {
labels: months,
datasets: [
{
label: '购买订单',
data: buyData,
borderColor: '#0d6efd',
backgroundColor: 'rgba(13, 110, 253, 0.1)',
tension: 0.3,
fill: true
},
{
label: '出售订单',
data: sellData,
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.3,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
});
</script>
{% endblock %}
app\templates\user\change_password.html
{% extends "base.html" %}
{% block title %}修改密码 - {{ super() }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-key me-2"></i>修改密码</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('user.change_password') }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.current_password.label(class="form-label") }}
{% if form.current_password.errors %}
{{ form.current_password(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.current_password.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.current_password(class="form-control") }}
{% endif %}
</div>
<div class="mb-3">
{{ form.new_password.label(class="form-label") }}
{% if form.new_password.errors %}
{{ form.new_password(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.new_password.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.new_password(class="form-control") }}
<div class="form-text">
密码长度至少为6个字符
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.confirm_password.label(class="form-label") }}
{% if form.confirm_password.errors %}
{{ form.confirm_password(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.confirm_password.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.confirm_password(class="form-control") }}
{% endif %}
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('user.profile', username=current_user.username) }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>返回
</a>
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\user\dashboard.html
{% extends "base.html" %}
{% block title %}用户控制面板 - {{ super() }}{% endblock %}
{% block content %}
<div class="row">
<!-- 侧边栏 -->
<div class="col-md-3 mb-4">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-user me-2"></i>{{ current_user.username }}</h5>
</div>
<div class="card-body text-center">
<img src="{{ current_user.get_avatar_url() }}" alt="头像" class="img-fluid rounded-circle avatar-medium mb-3">
<div class="badge bg-success mb-3">
<i class="fas fa-star me-1"></i>信用评分: {{ current_user.credit_score }}
</div>
<div class="list-group">
<a href="#overview" class="list-group-item list-group-item-action active" data-bs-toggle="list">
<i class="fas fa-tachometer-alt me-2"></i>总览
</a>
<a href="#my-items" class="list-group-item list-group-item-action" data-bs-toggle="list">
<i class="fas fa-shopping-bag me-2"></i>我的商品
</a>
<a href="#my-orders" class="list-group-item list-group-item-action" data-bs-toggle="list">
<i class="fas fa-shopping-cart me-2"></i>我的订单
</a>
<a href="#sold-items" class="list-group-item list-group-item-action" data-bs-toggle="list">
<i class="fas fa-hand-holding-usd me-2"></i>已售商品
</a>
<a href="#favorites" class="list-group-item list-group-item-action" data-bs-toggle="list">
<i class="fas fa-heart me-2"></i>我的收藏
</a>
<a href="{{ url_for('user.profile', username=current_user.username) }}" class="list-group-item list-group-item-action">
<i class="fas fa-user-circle me-2"></i>个人资料
</a>
</div>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9">
<div class="tab-content">
<!-- 总览面板 -->
<div class="tab-pane fade show active" id="overview">
<div class="card shadow mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-tachometer-alt me-2"></i>总览</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-3 col-6 mb-4">
<div class="p-3 bg-light rounded">
<i class="fas fa-shopping-bag fa-2x text-primary mb-2"></i>
<h4>{{ current_user.items.count() }}</h4>
<p class="text-muted mb-0">发布商品</p>
</div>
</div>
<div class="col-md-3 col-6 mb-4">
<div class="p-3 bg-light rounded">
<i class="fas fa-shopping-cart fa-2x text-success mb-2"></i>
<h4>{{ current_user.orders.count() }}</h4>
<p class="text-muted mb-0">购买订单</p>
</div>
</div>
<div class="col-md-3 col-6 mb-4">
<div class="p-3 bg-light rounded">
<i class="fas fa-hand-holding-usd fa-2x text-danger mb-2"></i>
<h4>{{ current_user.sold_orders.count() }}</h4>
<p class="text-muted mb-0">已售商品</p>
</div>
</div>
<div class="col-md-3 col-6 mb-4">
<div class="p-3 bg-light rounded">
<i class="fas fa-heart fa-2x text-info mb-2"></i>
<h4>{{ current_user.favorites.count() }}</h4>
<p class="text-muted mb-0">收藏商品</p>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>欢迎使用校园二手物品交易平台!您可以在这里管理您的所有交易活动。
</div>
{% if unread_messages_count > 0 %}
<div class="alert alert-warning">
<i class="fas fa-envelope me-2"></i>您有 <strong>{{ unread_messages_count }}</strong> 条未读消息,
<a href="#" class="alert-link">点击查看</a>
</div>
{% endif %}
</div>
</div>
<!-- 最近活动 -->
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-history me-2"></i>最近活动</h5>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% if user_orders or sold_orders or user_items %}
{% for order in user_orders[:3] %}
<a href="#" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">购买了商品 "{{ order.item.title }}"</h6>
<small>{{ order.created_at.strftime('%Y-%m-%d') }}</small>
</div>
<p class="mb-1">交易金额: ¥{{ order.price }}</p>
</a>
{% endfor %}
{% for order in sold_orders[:3] %}
<a href="#" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">售出商品 "{{ order.item.title }}"</h6>
<small>{{ order.created_at.strftime('%Y-%m-%d') }}</small>
</div>
<p class="mb-1">交易金额: ¥{{ order.price }}</p>
</a>
{% endfor %}
{% for item in user_items[:3] %}
<a href="#" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">发布了商品 "{{ item.title }}"</h6>
<small>{{ item.created_at.strftime('%Y-%m-%d') }}</small>
</div>
<p class="mb-1">价格: ¥{{ item.price }}</p>
</a>
{% endfor %}
{% else %}
<div class="list-group-item text-center py-5">
<i class="fas fa-history fa-3x text-muted mb-3"></i>
<h5>暂无活动记录</h5>
<p>您还没有任何交易活动,开始您的第一笔交易吧!</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 我的商品面板 -->
<div class="tab-pane fade" id="my-items">
<div class="card shadow">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-shopping-bag me-2"></i>我的商品</h5>
<a href="#" class="btn btn-light btn-sm">
<i class="fas fa-plus me-1"></i>发布新商品
</a>
</div>
<div class="card-body">
{% if user_items %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>商品</th>
<th>价格</th>
<th>状态</th>
<th>发布时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in user_items %}
<tr>
<td>
<div class="d-flex align-items-center">
<img src="{{ item.get_main_image_url() }}" alt="{{ item.title }}" class="img-thumbnail me-2" style="width: 50px; height: 50px; object-fit: cover;">
<div>{{ item.title }}</div>
</div>
</td>
<td>¥{{ item.price }}</td>
<td>
{% if not item.is_sold %}
<span class="badge bg-success">在售</span>
{% else %}
<span class="badge bg-secondary">已售出</span>
{% endif %}
</td>
<td>{{ item.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="#" class="btn btn-outline-primary">查看</a>
{% if not item.is_sold %}
<a href="#" class="btn btn-outline-secondary">编辑</a>
<a href="#" class="btn btn-outline-danger">下架</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="text-center mt-3">
<a href="#" class="btn btn-primary">查看全部</a>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-shopping-bag fa-4x text-muted mb-3"></i>
<h5>暂无发布的商品</h5>
<p>您还没有发布任何商品,立即发布您的第一个商品吧!</p>
<a href="#" class="btn btn-primary mt-2">
<i class="fas fa-plus me-1"></i>发布商品
</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 我的订单面板 -->
<div class="tab-pane fade" id="my-orders">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-shopping-cart me-2"></i>我的订单</h5>
</div>
<div class="card-body">
{% if user_orders %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>订单号</th>
<th>商品</th>
<th>卖家</th>
<th>价格</th>
<th>状态</th>
<th>下单时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for order in user_orders %}
<tr>
<td>{{ order.order_number }}</td>
<td>
<div class="d-flex align-items-center">
<img src="{{ order.item.get_main_image_url() }}" alt="{{ order.item.title }}" class="img-thumbnail me-2" style="width: 50px; height: 50px; object-fit: cover;">
<div>{{ order.item.title }}</div>
</div>
</td>
<td>
<a href="{{ url_for('user.profile', username=order.seller.username) }}">
{{ order.seller.username }}
</a>
</td>
<td>¥{{ order.price }}</td>
<td>
{% if order.status == 'pending' %}
<span class="badge bg-warning">待付款</span>
{% elif order.status == 'paid' %}
<span class="badge bg-info">已付款</span>
{% elif order.status == 'completed' %}
<span class="badge bg-success">已完成</span>
{% elif order.status == 'cancelled' %}
<span class="badge bg-danger">已取消</span>
{% endif %}
</td>
<td>{{ order.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="#" class="btn btn-outline-primary">查看</a>
{% if order.status == 'pending' %}
<a href="#" class="btn btn-outline-success">付款</a>
<a href="#" class="btn btn-outline-danger">取消</a>
{% elif order.status == 'paid' %}
<a href="#" class="btn btn-outline-success">确认收货</a>
{% elif order.status == 'completed' and not order.is_reviewed %}
<a href="#" class="btn btn-outline-primary">评价</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="text-center mt-3">
<a href="#" class="btn btn-primary">查看全部</a>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-shopping-cart fa-4x text-muted mb-3"></i>
<h5>暂无订单</h5>
<p>您还没有购买任何商品,去浏览一下吧!</p>
<a href="#" class="btn btn-primary mt-2">
<i class="fas fa-search me-1"></i>浏览商品
</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 已售商品面板 -->
<div class="tab-pane fade" id="sold-items">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-hand-holding-usd me-2"></i>已售商品</h5>
</div>
<div class="card-body">
{% if sold_orders %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>订单号</th>
<th>商品</th>
<th>买家</th>
<th>价格</th>
<th>状态</th>
<th>下单时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for order in sold_orders %}
<tr>
<td>{{ order.order_number }}</td>
<td>
<div class="d-flex align-items-center">
<img src="{{ order.item.get_main_image_url() }}" alt="{{ order.item.title }}" class="img-thumbnail me-2" style="width: 50px; height: 50px; object-fit: cover;">
<div>{{ order.item.title }}</div>
</div>
</td>
<td>
<a href="{{ url_for('user.profile', username=order.buyer.username) }}">
{{ order.buyer.username }}
</a>
</td>
<td>¥{{ order.price }}</td>
<td>
{% if order.status == 'pending' %}
<span class="badge bg-warning">待付款</span>
{% elif order.status == 'paid' %}
<span class="badge bg-info">已付款</span>
{% elif order.status == 'completed' %}
<span class="badge bg-success">已完成</span>
{% elif order.status == 'cancelled' %}
<span class="badge bg-danger">已取消</span>
{% endif %}
</td>
<td>{{ order.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="#" class="btn btn-outline-primary">查看</a>
{% if order.status == 'paid' %}
<a href="#" class="btn btn-outline-success">确认发货</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="text-center mt-3">
<a href="#" class="btn btn-primary">查看全部</a>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-hand-holding-usd fa-4x text-muted mb-3"></i>
<h5>暂无已售商品</h5>
<p>您还没有售出任何商品</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 我的收藏面板 -->
<div class="tab-pane fade" id="favorites">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-heart me-2"></i>我的收藏</h5>
</div>
<div class="card-body">
{% if favorites %}
<div class="row">
{% for favorite in favorites %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<img src="{{ favorite.item.get_main_image_url() }}" class="card-img-top item-thumbnail" alt="{{ favorite.item.title }}">
<div class="card-body">
<h5 class="card-title">{{ favorite.item.title }}</h5>
<p class="card-text text-danger fw-bold">¥{{ favorite.item.price }}</p>
<p class="card-text small text-muted">
<i class="fas fa-user me-1"></i>{{ favorite.item.seller.username }}
<i class="fas fa-clock ms-2 me-1"></i>{{ favorite.item.created_at.strftime('%Y-%m-%d') }}
</p>
</div>
<div class="card-footer bg-white">
<div class="d-flex justify-content-between">
<a href="#" class="btn btn-sm btn-primary">查看详情</a>
<a href="#" class="btn btn-sm btn-outline-danger">
<i class="fas fa-heart"></i>
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="text-center mt-3">
<a href="#" class="btn btn-primary">查看全部</a>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-heart fa-4x text-muted mb-3"></i>
<h5>暂无收藏商品</h5>
<p>您还没有收藏任何商品,去浏览一下吧!</p>
<a href="#" class="btn btn-primary mt-2">
<i class="fas fa-search me-1"></i>浏览商品
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
app\templates\user\edit_profile.html
{% extends "base.html" %}
{% block title %}编辑个人资料 - {{ super() }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-user-edit me-2"></i>编辑个人资料</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('user.edit_profile') }}" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="row mb-4">
<div class="col-md-3 text-center">
<img src="{{ current_user.get_avatar_url() }}" alt="当前头像" class="img-fluid rounded-circle avatar-medium mb-2">
<p class="small text-muted">当前头像</p>
</div>
<div class="col-md-9">
<div class="mb-3">
{{ form.avatar.label(class="form-label") }}
{{ form.avatar(class="form-control") }}
{% if form.avatar.errors %}
<div class="invalid-feedback d-block">
{% for error in form.avatar.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
<div class="form-text">
支持JPG、JPEG、PNG和GIF格式,最大文件大小为5MB
</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{% if form.username.errors %}
{{ form.username(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.username.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.username(class="form-control") }}
{% endif %}
</div>
<div class="mb-3">
{{ form.phone.label(class="form-label") }}
{% if form.phone.errors %}
{{ form.phone(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.phone.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.phone(class="form-control") }}
{% endif %}
</div>
<div class="mb-3">
{{ form.dormitory.label(class="form-label") }}
{% if form.dormitory.errors %}
{{ form.dormitory(class="form-control is-invalid") }}
<div class="invalid-feedback">
{% for error in form.dormitory.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.dormitory(class="form-control", placeholder="例如:X栋XXX") }}
{% endif %}
</div>
<div class="mb-3">
{{ form.bio.label(class="form-label") }}
{% if form.bio.errors %}
{{ form.bio(class="form-control is-invalid", rows=4) }}
<div class="invalid-feedback">
{% for error in form.bio.errors %}
{{ error }}
{% endfor %}
</div>
{% else %}
{{ form.bio(class="form-control", rows=4, placeholder="介绍一下自己吧...") }}
{% endif %}
<div class="form-text text-end">
<span id="bioCounter">0</span>/200
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('user.profile', username=current_user.username) }}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>返回
</a>
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// 字数统计
document.addEventListener('DOMContentLoaded', function() {
const bioTextarea = document.getElementById('bio');
const bioCounter = document.getElementById('bioCounter');
if (bioTextarea && bioCounter) {
// 初始化计数
bioCounter.textContent = bioTextarea.value.length;
// 监听输入事件
bioTextarea.addEventListener('input', function() {
bioCounter.textContent = this.value.length;
// 超出字数限制时显示警告
if (this.value.length > 200) {
bioCounter.classList.add('text-danger');
} else {
bioCounter.classList.remove('text-danger');
}
});
}
});
</script>
{% endblock %}
app\templates\user\profile.html
{% extends "base.html" %}
{% block title %}{{ user.username }}的个人资料 - {{ super() }}{% endblock %}
{% block content %}
<div class="row">
<!-- 用户资料卡片 -->
<div class="col-md-4 mb-4">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-user me-2"></i>个人资料</h4>
</div>
<div class="card-body text-center">
<img src="{{ user.get_avatar_url() }}" alt="{{ user.username }}的头像" class="img-fluid rounded-circle avatar-large mb-3">
<h4>{{ user.username }}</h4>
<div class="d-flex justify-content-center mb-3">
<div class="badge bg-success me-2">
<i class="fas fa-star me-1"></i>信用评分: {{ user.credit_score }}
</div>
{% if user.is_admin %}
<div class="badge bg-danger">
<i class="fas fa-shield-alt me-1"></i>管理员
</div>
{% endif %}
</div>
{% if user.bio %}
<p class="card-text">{{ user.bio }}</p>
{% else %}
<p class="card-text text-muted">这个用户很懒,还没有填写个人简介</p>
{% endif %}
<hr>
<div class="user-info">
<p><i class="fas fa-envelope me-2"></i>{{ user.email }}</p>
{% if user.phone %}
<p><i class="fas fa-phone me-2"></i>{{ user.phone }}</p>
{% endif %}
{% if user.dormitory %}
<p><i class="fas fa-home me-2"></i>{{ user.dormitory }}</p>
{% endif %}
<p><i class="fas fa-calendar-alt me-2"></i>注册于: {{ user.created_at.strftime('%Y-%m-%d') }}</p>
<p><i class="fas fa-clock me-2"></i>最后在线: {{ user.last_seen.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
{% if current_user.is_authenticated and current_user.id == user.id %}
<div class="mt-3">
<a href="{{ url_for('user.edit_profile') }}" class="btn btn-primary">
<i class="fas fa-edit me-1"></i>编辑资料
</a>
<a href="{{ url_for('user.change_password') }}" class="btn btn-outline-secondary">
<i class="fas fa-key me-1"></i>修改密码
</a>
</div>
{% endif %}
</div>
</div>
<!-- 用户统计信息 -->
<div class="card shadow mt-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>统计信息</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-4">
<h5>{{ user.items.count() }}</h5>
<small class="text-muted">发布商品</small>
</div>
<div class="col-4">
<h5>{{ user.sold_orders.count() }}</h5>
<small class="text-muted">已售出</small>
</div>
<div class="col-4">
<h5>{{ user.reviews_received.count() }}</h5>
<small class="text-muted">收到评价</small>
</div>
</div>
</div>
</div>
</div>
<!-- 用户发布的商品 -->
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="fas fa-shopping-bag me-2"></i>{{ user.username }}发布的商品</h4>
</div>
<div class="card-body">
{% if items.items %}
<div class="row">
{% for item in items.items %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<img src="{{ item.get_main_image_url() }}" class="card-img-top item-thumbnail" alt="{{ item.title }}">
<div class="card-body">
<h5 class="card-title">{{ item.title }}</h5>
<p class="card-text text-danger fw-bold">¥{{ item.price }}</p>
<p class="card-text small text-muted">
<i class="fas fa-clock me-1"></i>{{ item.created_at.strftime('%Y-%m-%d') }}
</p>
</div>
<div class="card-footer bg-white">
<a href="#" class="btn btn-sm btn-primary">查看详情</a>
{% if not item.is_sold %}
<span class="badge bg-success float-end mt-1">在售</span>
{% else %}
<span class="badge bg-secondary float-end mt-1">已售出</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<!-- 分页 -->
{% if items.pages > 1 %}
<nav aria-label="商品分页">
<ul class="pagination justify-content-center">
{% if items.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('user.profile', username=user.username, page=items.prev_num) }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link"><i class="fas fa-chevron-left"></i></span>
</li>
{% endif %}
{% for page in items.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if page %}
{% if page == items.page %}
<li class="page-item active">
<span class="page-link">{{ page }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('user.profile', username=user.username, page=page) }}">{{ page }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if items.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('user.profile', username=user.username, page=items.next_num) }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link"><i class="fas fa-chevron-right"></i></span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-shopping-bag fa-4x text-muted mb-3"></i>
<h5>暂无发布的商品</h5>
{% if current_user.is_authenticated and current_user.id == user.id %}
<p>您还没有发布任何商品,立即发布您的第一个商品吧!</p>
<a href="#" class="btn btn-primary mt-2">
<i class="fas fa-plus me-1"></i>发布商品
</a>
{% else %}
<p>该用户暂未发布任何商品</p>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
app\utils\decorators.py
from functools import wraps
from flask import flash, redirect, url_for
from flask_login import current_user
def check_confirmed(func):
"""检查用户是否已确认邮箱的装饰器"""
@wraps(func)
def decorated_function(*args, **kwargs):
# 如果用户未确认邮箱且不是管理员
if hasattr(current_user, 'is_confirmed') and not current_user.is_confirmed and not current_user.is_admin:
flash('请先确认您的邮箱!', 'warning')
return redirect(url_for('auth.unconfirmed'))
return func(*args, **kwargs)
return decorated_function
def admin_required(func):
"""检查用户是否是管理员的装饰器"""
@wraps(func)
def decorated_function(*args, **kwargs):
if not current_user.is_admin:
flash('您没有权限访问此页面', 'danger')
return redirect(url_for('main.index'))
return func(*args, **kwargs)
return decorated_function
app\utils\email.py
from flask import current_app, render_template
from flask_mail import Message
from threading import Thread
from app import mail
def send_async_email(app, msg):
"""异步发送邮件"""
with app.app_context():
mail.send(msg)
def send_email(subject, recipients, text_body, html_body, sender=None):
"""发送邮件"""
msg = Message(subject, recipients=recipients, sender=sender)
msg.body = text_body
msg.html = html_body
# 获取当前应用实例
app = current_app._get_current_object()
# 创建线程异步发送邮件
Thread(target=send_async_email, args=(app, msg)).start()
def send_password_reset_email(user):
"""发送密码重置邮件"""
# 生成令牌
token = user.get_reset_password_token()
# 发送邮件
send_email(
subject='【校园二手交易平台】重置您的密码',
recipients=[user.email],
text_body=render_template('email/reset_password.txt', user=user, token=token),
html_body=render_template('email/reset_password.html', user=user, token=token)
)
config\config.py
import os
from datetime import timedelta
class Config:
"""应用配置类"""
# 基本配置
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///campus_trading.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 上传文件配置
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'app', 'static', 'uploads')
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 最大上传文件大小 (16MB)
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
# 邮件配置
MAIL_SERVER = os.environ.get('MAIL_SERVER') or 'smtp.example.com'
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') or True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@example.com'
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-password'
MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER') or 'your-email@example.com'
# 分页配置
ITEMS_PER_PAGE = 12
# 会话配置
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
# 用户头像默认配置
DEFAULT_AVATAR = 'default_avatar.png'
# 系统管理员配置
ADMIN_EMAIL = os.environ.get('ADMIN_EMAIL') or 'admin@example.com'
class DevelopmentConfig(Config):
"""开发环境配置"""
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'sqlite:///campus_trading_dev.db'
class TestingConfig(Config):
"""测试环境配置"""
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or 'sqlite:///campus_trading_test.db'
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
"""生产环境配置"""
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///campus_trading.db'
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
database\schema.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(64) NOT NULL COMMENT '用户名',
`email` varchar(120) NOT NULL COMMENT '邮箱',
`password_hash` varchar(128) NOT NULL COMMENT '密码哈希',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`student_id` varchar(20) DEFAULT NULL COMMENT '学号',
`real_name` varchar(64) DEFAULT NULL COMMENT '真实姓名',
`college` varchar(64) DEFAULT NULL COMMENT '学院',
`major` varchar(64) DEFAULT NULL COMMENT '专业',
`bio` text DEFAULT NULL COMMENT '个人简介',
`is_verified` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否验证',
`is_admin` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否管理员',
`credit_score` int(11) NOT NULL DEFAULT 100 COMMENT '信用分',
`balance` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '账户余额',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`last_login` datetime DEFAULT NULL COMMENT '最后登录时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ix_user_username` (`username`),
UNIQUE KEY `ix_user_email` (`email`),
UNIQUE KEY `ix_user_student_id` (`student_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
DROP TABLE IF EXISTS `user_address`;
CREATE TABLE `user_address` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '地址ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`recipient` varchar(64) NOT NULL COMMENT '收件人',
`phone` varchar(20) NOT NULL COMMENT '联系电话',
`province` varchar(32) NOT NULL COMMENT '省',
`city` varchar(32) NOT NULL COMMENT '市',
`district` varchar(32) NOT NULL COMMENT '区',
`address` varchar(255) NOT NULL COMMENT '详细地址',
`is_default` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否默认地址',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `ix_user_address_user_id` (`user_id`),
CONSTRAINT `fk_user_address_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户地址表';
DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`name` varchar(64) NOT NULL COMMENT '分类名称',
`description` text DEFAULT NULL COMMENT '分类描述',
`parent_id` int(11) DEFAULT NULL COMMENT '父分类ID',
`icon` varchar(255) DEFAULT NULL COMMENT '分类图标',
`sort_order` int(11) NOT NULL DEFAULT 0 COMMENT '排序',
`is_active` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否激活',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `ix_category_parent_id` (`parent_id`),
CONSTRAINT `fk_category_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `category` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表';
DROP TABLE IF EXISTS `item`;
CREATE TABLE `item` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`title` varchar(128) NOT NULL COMMENT '商品标题',
`description` text DEFAULT NULL COMMENT '商品描述',
`price` decimal(10, 2) NOT NULL COMMENT '商品价格',
`original_price` decimal(10, 2) DEFAULT NULL COMMENT '原价',
`category_id` int(11) NOT NULL COMMENT '分类ID',
`user_id` int(11) NOT NULL COMMENT '发布用户ID',
`condition` varchar(32) NOT NULL COMMENT '商品状况',
`status` varchar(32) NOT NULL DEFAULT 'active' COMMENT '商品状态',
`location` varchar(255) DEFAULT NULL COMMENT '交易地点',
`views` int(11) NOT NULL DEFAULT 0 COMMENT '浏览次数',
`favorites` int(11) NOT NULL DEFAULT 0 COMMENT '收藏次数',
`is_negotiable` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否可议价',
`is_featured` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否推荐',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `ix_item_category_id` (`category_id`),
KEY `ix_item_user_id` (`user_id`),
KEY `ix_item_status` (`status`),
CONSTRAINT `fk_item_category_id` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`),
CONSTRAINT `fk_item_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
DROP TABLE IF EXISTS `item_image`;
CREATE TABLE `item_image` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '图片ID',
`item_id` int(11) NOT NULL COMMENT '商品ID',
`image_path` varchar(255) NOT NULL COMMENT '图片路径',
`is_main` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否主图',
`sort_order` int(11) NOT NULL DEFAULT 0 COMMENT '排序',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `ix_item_image_item_id` (`item_id`),
CONSTRAINT `fk_item_image_item_id` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品图片表';
DROP TABLE IF EXISTS `tag`;
CREATE TABLE `tag` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '标签ID',
`name` varchar(64) NOT NULL COMMENT '标签名称',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ix_tag_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品标签表';
DROP TABLE IF EXISTS `item_tag`;
CREATE TABLE `item_tag` (
`item_id` int(11) NOT NULL COMMENT '商品ID',
`tag_id` int(11) NOT NULL COMMENT '标签ID',
PRIMARY KEY (`item_id`, `tag_id`),
KEY `ix_item_tag_tag_id` (`tag_id`),
CONSTRAINT `fk_item_tag_item_id` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_item_tag_tag_id` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品标签关联表';
DROP TABLE IF EXISTS `favorite`;
CREATE TABLE `favorite` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '收藏ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`item_id` int(11) NOT NULL COMMENT '商品ID',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ix_favorite_user_item` (`user_id`, `item_id`),
KEY `ix_favorite_item_id` (`item_id`),
CONSTRAINT `fk_favorite_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_favorite_item_id` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表';
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_number` varchar(32) NOT NULL COMMENT '订单编号',
`buyer_id` int(11) NOT NULL COMMENT '买家ID',
`seller_id` int(11) NOT NULL COMMENT '卖家ID',
`item_id` int(11) NOT NULL COMMENT '商品ID',
`price` decimal(10, 2) NOT NULL COMMENT '成交价格',
`status` varchar(32) NOT NULL DEFAULT 'pending' COMMENT '订单状态',
`payment_method` varchar(32) DEFAULT NULL COMMENT '支付方式',
`payment_id` varchar(64) DEFAULT NULL COMMENT '支付ID',
`address_id` int(11) DEFAULT NULL COMMENT '收货地址ID',
`shipping_method` varchar(32) DEFAULT NULL COMMENT '配送方式',
`tracking_number` varchar(64) DEFAULT NULL COMMENT '物流单号',
`transaction_fee` decimal(10, 2) DEFAULT 0.00 COMMENT '交易手续费',
`remark` text DEFAULT NULL COMMENT '订单备注',
`paid_at` datetime DEFAULT NULL COMMENT '支付时间',
`shipped_at` datetime DEFAULT NULL COMMENT '发货时间',
`completed_at` datetime DEFAULT NULL COMMENT '完成时间',
`cancelled_at` datetime DEFAULT NULL COMMENT '取消时间',
`cancel_reason` varchar(255) DEFAULT NULL COMMENT '取消原因',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ix_order_order_number` (`order_number`),
KEY `ix_order_buyer_id` (`buyer_id`),
KEY `ix_order_seller_id` (`seller_id`),
KEY `ix_order_item_id` (`item_id`),
KEY `ix_order_status` (`status`),
KEY `ix_order_address_id` (`address_id`),
CONSTRAINT `fk_order_buyer_id` FOREIGN KEY (`buyer_id`) REFERENCES `user` (`id`),
CONSTRAINT `fk_order_seller_id` FOREIGN KEY (`seller_id`) REFERENCES `user` (`id`),
CONSTRAINT `fk_order_item_id` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`),
CONSTRAINT `fk_order_address_id` FOREIGN KEY (`address_id`) REFERENCES `user_address` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
DROP TABLE IF EXISTS `dispute`;
CREATE TABLE `dispute` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '纠纷ID',
`order_id` int(11) NOT NULL COMMENT '订单ID',
`initiator_id` int(11) NOT NULL COMMENT '发起人ID',
`reason` varchar(64) NOT NULL COMMENT '纠纷原因',
`description` text NOT NULL COMMENT '问题描述',
`preferred_solution` varchar(32) NOT NULL COMMENT '期望解决方案',
`status` varchar(32) NOT NULL DEFAULT 'pending' COMMENT '纠纷状态',
`admin_id` int(11) DEFAULT NULL COMMENT '处理管理员ID',
`admin_notes` text DEFAULT NULL COMMENT '管理员备注',
`resolution` text DEFAULT NULL COMMENT '解决方案',
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`closed_at` datetime DEFAULT NULL COMMENT '关闭时间',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `ix_dispute_order_id` (`order_id`),
KEY `ix_dispute_initiator_id` (`initiator_id`),
KEY `ix_dispute_admin_id` (`admin_id`),
KEY `ix_dispute_status` (`status`),
CONSTRAINT `fk_dispute_order_id` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`),
CONSTRAINT `fk_dispute_initiator_id` FOREIGN KEY (`initiator_id`) REFERENCES `user` (`id`),
CONSTRAINT `fk_dispute_admin_id` FOREIGN KEY (`admin_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易纠纷表';
DROP TABLE IF EXISTS `dispute_evidence`;
CREATE TABLE `dispute_evidence` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '证据ID',
`dispute_id` int(11) NOT NULL COMMENT '纠纷ID',
`image_path` varchar(255) NOT NULL COMMENT '图片路径',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `ix_dispute_evidence_dispute_id` (`dispute_id`),
CONSTRAINT `fk_dispute_evidence_dispute_id` FOREIGN KEY (`dispute_id`) REFERENCES `dispute` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='纠纷证据图片表';
DROP TABLE IF EXISTS `review`;
CREATE TABLE `review` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '评价ID',
`order_id` int(11) NOT NULL COMMENT '订单ID',
`reviewer_id` int(11) NOT NULL COMMENT '评价人ID',
`reviewee_id` int(11) NOT NULL COMMENT '被评价人ID',
`rating` int(11) NOT NULL COMMENT '评分',
`content` text DEFAULT NULL COMMENT '评价内容',
`is_anonymous` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否匿名',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ix_review_order_reviewer` (`order_id`, `reviewer_id`),
KEY `ix_review_reviewer_id` (`reviewer_id`),
KEY `ix_review_reviewee_id` (`reviewee_id`),
CONSTRAINT `fk_review_order_id` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`),
CONSTRAINT `fk_review_reviewer_id` FOREIGN KEY (`reviewer_id`) REFERENCES `user` (`id`),
CONSTRAINT `fk_review_reviewee_id` FOREIGN KEY (`reviewee_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评价表';
DROP TABLE IF EXISTS `message`;
CREATE TABLE `message` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '消息ID',
`sender_id` int(11) NOT NULL COMMENT '发送者ID',
`receiver_id` int(11) NOT NULL COMMENT '接收者ID',
`content` text NOT NULL COMMENT '消息内容',
`is_read` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已读',
`item_id` int(11) DEFAULT NULL COMMENT '相关商品ID',
`order_id` int(11) DEFAULT NULL COMMENT '相关订单ID',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `ix_message_sender_id` (`sender_id`),
KEY `ix_message_receiver_id` (`receiver_id`),
KEY `ix_message_item_id` (`item_id`),
KEY `ix_message_order_id` (`order_id`),
CONSTRAINT `fk_message_sender_id` FOREIGN KEY (`sender_id`) REFERENCES `user` (`id`),
CONSTRAINT `fk_message_receiver_id` FOREIGN KEY (`receiver_id`) REFERENCES `user` (`id`),
CONSTRAINT `fk_message_item_id` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`) ON DELETE SET NULL,
CONSTRAINT `fk_message_order_id` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';
DROP TABLE IF EXISTS `notification`;
CREATE TABLE `notification` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '通知ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`type` varchar(32) NOT NULL COMMENT '通知类型',
`title` varchar(128) NOT NULL COMMENT '通知标题',
`content` text NOT NULL COMMENT '通知内容',
`is_read` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已读',
`related_id` int(11) DEFAULT NULL COMMENT '相关ID',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `ix_notification_user_id` (`user_id`),
KEY `ix_notification_type` (`type`),
CONSTRAINT `fk_notification_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统通知表';
DROP TABLE IF EXISTS `transaction`;
CREATE TABLE `transaction` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '交易ID',
`transaction_number` varchar(32) NOT NULL COMMENT '交易编号',
`order_id` int(11) NOT NULL COMMENT '订单ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`amount` decimal(10, 2) NOT NULL COMMENT '交易金额',
`type` varchar(32) NOT NULL COMMENT '交易类型',
`payment_method` varchar(32) DEFAULT NULL COMMENT '支付方式',
`payment_id` varchar(64) DEFAULT NULL COMMENT '支付ID',
`status` varchar(32) NOT NULL COMMENT '交易状态',
`description` varchar(255) DEFAULT NULL COMMENT '交易描述',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ix_transaction_transaction_number` (`transaction_number`),
KEY `ix_transaction_order_id` (`order_id`),
KEY `ix_transaction_user_id` (`user_id`),
KEY `ix_transaction_type` (`type`),
KEY `ix_transaction_status` (`status`),
CONSTRAINT `fk_transaction_order_id` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`),
CONSTRAINT `fk_transaction_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录表';
DROP TABLE IF EXISTS `search_history`;
CREATE TABLE `search_history` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '搜索历史ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`keyword` varchar(128) NOT NULL COMMENT '搜索关键词',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `ix_search_history_user_id` (`user_id`),
CONSTRAINT `fk_search_history_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='搜索历史表';
DROP TABLE IF EXISTS `browse_history`;
CREATE TABLE `browse_history` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '浏览历史ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`item_id` int(11) NOT NULL COMMENT '商品ID',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `ix_browse_history_user_id` (`user_id`),
KEY `ix_browse_history_item_id` (`item_id`),
CONSTRAINT `fk_browse_history_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_browse_history_item_id` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='浏览历史表';
DROP TABLE IF EXISTS `setting`;
CREATE TABLE `setting` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '设置ID',
`key` varchar(64) NOT NULL COMMENT '设置键',
`value` text NOT NULL COMMENT '设置值',
`description` varchar(255) DEFAULT NULL COMMENT '设置描述',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ix_setting_key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统设置表';
DROP TABLE IF EXISTS `feedback`;
CREATE TABLE `feedback` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '反馈ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`type` varchar(32) NOT NULL COMMENT '反馈类型',
`content` text NOT NULL COMMENT '反馈内容',
`status` varchar(32) NOT NULL DEFAULT 'pending' COMMENT '处理状态',
`admin_id` int(11) DEFAULT NULL COMMENT '处理管理员ID',
`reply` text DEFAULT NULL COMMENT '回复内容',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `ix_feedback_user_id` (`user_id`),
KEY `ix_feedback_admin_id` (`admin_id`),
KEY `ix_feedback_status` (`status`),
CONSTRAINT `fk_feedback_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
CONSTRAINT `fk_feedback_admin_id` FOREIGN KEY (`admin_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='反馈表';
INSERT INTO `user` (`username`, `email`, `password_hash`, `is_verified`, `is_admin`, `created_at`, `updated_at`)
VALUES ('admin', 'admin@example.com', '$2b$12$tJlK7X0ZQG3CSU1QjQ5Qn.Vc.0Uo7DpHgO6hCsGtDYXkGNwSsUfwG', 1, 1, NOW(), NOW());
INSERT INTO `category` (`name`, `description`, `icon`, `sort_order`, `created_at`, `updated_at`) VALUES
('教材教辅', '各类教材、教辅资料、考试资料等', 'book', 1, NOW(), NOW()),
('电子产品', '手机、电脑、平板等电子设备', 'laptop', 2, NOW(), NOW()),
('生活用品', '日常生活用品、宿舍用品等', 'home', 3, NOW(), NOW()),
('服装鞋帽', '各类服装、鞋子、帽子等', 'tshirt', 4, NOW(), NOW()),
('运动健身', '运动器材、健身装备等', 'dumbbell', 5, NOW(), NOW()),
('美妆护肤', '化妆品、护肤品等', 'heart', 6, NOW(), NOW()),
('票券卡劵', '各类票券、卡券、会员卡等', 'ticket', 7, NOW(), NOW()),
('其他物品', '其他二手物品', 'box', 8, NOW(), NOW());
INSERT INTO `category` (`name`, `description`, `parent_id`, `icon`, `sort_order`, `created_at`, `updated_at`)
SELECT '专业教材', '各专业课程教材', id, 'book-half', 1, NOW(), NOW() FROM `category` WHERE `name` = '教材教辅';
INSERT INTO `category` (`name`, `description`, `parent_id`, `icon`, `sort_order`, `created_at`, `updated_at`)
SELECT '考研资料', '考研复习资料、辅导书等', id, 'journal-bookmark', 2, NOW(), NOW() FROM `category` WHERE `name` = '教材教辅';
INSERT INTO `category` (`name`, `description`, `parent_id`, `icon`, `sort_order`, `created_at`, `updated_at`)
SELECT '手机', '各品牌二手手机', id, 'phone', 1, NOW(), NOW() FROM `category` WHERE `name` = '电子产品';
INSERT INTO `category` (`name`, `description`, `parent_id`, `icon`, `sort_order`, `created_at`, `updated_at`)
SELECT '电脑', '笔记本电脑、台式电脑等', id, 'laptop', 2, NOW(), NOW() FROM `category` WHERE `name` = '电子产品';
INSERT INTO `setting` (`key`, `value`, `description`, `created_at`, `updated_at`) VALUES
('site_name', '校园二手物品交易平台', '网站名称', NOW(), NOW()),
('site_description', '为校园师生提供便捷的二手物品交易服务', '网站描述', NOW(), NOW()),
('transaction_fee_rate', '0.02', '交易手续费率', NOW(), NOW()),
('max_item_images', '5', '每个商品最大图片数量', NOW(), NOW()),
('enable_dispute', '1', '是否启用纠纷功能', NOW(), NOW()),
('enable_message', '1', '是否启用站内信功能', NOW(), NOW()),
('enable_review', '1', '是否启用评价功能', NOW(), NOW());
SET FOREIGN_KEY_CHECKS = 1;