from flask import Flask, render_template, jsonify, request, g, abort
from database import get_db, init_db
import os
from datetime import datetime
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
STATUS_MAP = {
'open': '招标中', 'evaluating': '评审中', 'closed': '已结束',
'pending': '待提交', 'submitted': '已提交', 'won': '已中标', 'lost': '未中标',
}
PER_PAGE = 10
def status_text(status):
return STATUS_MAP.get(status, status)
def bid_status_text(status):
return STATUS_MAP.get(status, status)
app.jinja_env.globals['status_text'] = status_text
app.jinja_env.globals['bid_status_text'] = bid_status_text
@app.context_processor
def inject_globals():
return {'year': datetime.now().year, 'status_map': STATUS_MAP}
@app.before_request
def before_request():
g.db = get_db()
@app.teardown_request
def teardown_request(exc):
db = getattr(g, 'db', None)
if db is not None:
db.close()
def get_stats(db):
return {
'total_projects': db.execute('SELECT COUNT(*) as c FROM projects').fetchone()['c'],
'open_projects': db.execute("SELECT COUNT(*) as c FROM projects WHERE status='open'").fetchone()['c'],
'evaluating': db.execute("SELECT COUNT(*) as c FROM projects WHERE status='evaluating'").fetchone()['c'],
'closed': db.execute("SELECT COUNT(*) as c FROM projects WHERE status='closed'").fetchone()['c'],
'total_bids': db.execute('SELECT COUNT(*) as c FROM bids').fetchone()['c'],
'total_budget': db.execute('SELECT COALESCE(SUM(budget),0) as s FROM projects').fetchone()['s'],
}
def paginate(rows, page, per_page):
total = len(rows)
start = (page - 1) * per_page
return rows[start:start + per_page], total, (total + per_page - 1) // per_page
@app.route('/')
def index():
db = g.db
stats = get_stats(db)
recent = db.execute('SELECT * FROM projects ORDER BY created_at DESC LIMIT 5').fetchall()
return render_template('index.html', stats=stats, recent=recent)
@app.route('/api/stats')
def api_stats():
db = g.db
stats = get_stats(db)
by_category = db.execute(
'SELECT category, COUNT(*) as c, COALESCE(SUM(budget),0) as budget FROM projects GROUP BY category'
).fetchall()
stats['by_category'] = [dict(r) for r in by_category]
return jsonify(stats)
@app.route('/projects')
def projects():
db = g.db
status = request.args.get('status', '')
search = request.args.get('q', '').strip()
page = max(1, request.args.get('page', 1, type=int))
sql = 'SELECT * FROM projects WHERE 1=1'
params = []
if status:
sql += ' AND status=?'
params.append(status)
if search:
sql += ' AND (name LIKE ? OR code LIKE ?)'
params.extend([f'%{search}%', f'%{search}%'])
sql += ' ORDER BY created_at DESC'
rows = db.execute(sql, params).fetchall()
items, total, pages = paginate(rows, page, PER_PAGE)
return render_template('projects.html', projects=items, current_status=status,
search=search, page=page, pages=pages, total=total)
@app.route('/project/<int:pid>')
def project_detail(pid):
db = g.db
project = db.execute('SELECT * FROM projects WHERE id=?', (pid,)).fetchone()
if not project:
abort(404)
bids = db.execute(
'''SELECT b.*, bi.name as bidder_name, bi.company, bi.qualification,
e.technical_score, e.price_score, e.business_score, e.total_score,
e.evaluator, e.comment as eval_comment
FROM bids b
JOIN bidders bi ON b.bidder_id=bi.id
LEFT JOIN evaluations e ON e.bid_id=b.id
WHERE b.project_id=? ORDER BY b.quote_price ASC''', (pid,)
).fetchall()
return render_template('project_detail.html', project=project, bids=bids)
@app.route('/bidders')
def bidders():
db = g.db
search = request.args.get('q', '').strip()
if search:
rows = db.execute(
'SELECT * FROM bidders WHERE name LIKE ? OR company LIKE ? ORDER BY name',
(f'%{search}%', f'%{search}%')
).fetchall()
else:
rows = db.execute('SELECT * FROM bidders ORDER BY name').fetchall()
return render_template('bidders.html', bidders=rows, search=search)
@app.route('/statistics')
def statistics():
db = g.db
by_category = db.execute(
'SELECT category, COUNT(*) as c, COALESCE(SUM(budget),0) as budget FROM projects GROUP BY category'
).fetchall()
by_status = db.execute(
'SELECT status, COUNT(*) as c FROM projects GROUP BY status'
).fetchall()
avg_quote = db.execute(
'''SELECT p.name, AVG(b.quote_price) as avg_price, COUNT(b.id) as bid_count
FROM projects p LEFT JOIN bids b ON p.id=b.project_id
GROUP BY p.id ORDER BY avg_price DESC'''
).fetchall()
top_bidders = db.execute(
'''SELECT bi.name, COUNT(b.id) as bid_count, AVG(b.quote_price) as avg_price
FROM bidders bi LEFT JOIN bids b ON bi.id=b.bidder_id
GROUP BY bi.id ORDER BY bid_count DESC LIMIT 5'''
).fetchall()
return render_template('statistics.html', by_category=by_category, by_status=by_status,
avg_quote=avg_quote, top_bidders=top_bidders)
@app.route('/api/projects', methods=['GET', 'POST'])
def api_projects():
db = g.db
if request.method == 'POST':
data = request.get_json(silent=True) or {}
for k in ('name', 'code'):
if not data.get(k):
return jsonify({'error': f'缺少必填字段: {k}'}), 400
try:
cur = db.execute(
'''INSERT INTO projects (name, code, category, budget, status, publish_date, deadline, description)
VALUES (?,?,?,?,?,?,?,?)''',
(data['name'], data['code'], data.get('category', ''),
float(data.get('budget', 0)), data.get('status', 'open'),
data.get('publish_date', ''), data.get('deadline', ''), data.get('description', ''))
)
db.commit()
return jsonify({'id': cur.lastrowid, 'message': '创建成功'}), 201
except Exception as e:
db.rollback()
msg = str(e)
if 'UNIQUE constraint' in msg:
msg = '项目编号已存在,请使用其他编号'
return jsonify({'error': msg}), 400
rows = db.execute('SELECT * FROM projects ORDER BY created_at DESC').fetchall()
return jsonify([dict(r) for r in rows])
@app.route('/api/projects/<int:pid>', methods=['GET', 'PUT', 'DELETE'])
def api_project_detail(pid):
db = g.db
if request.method == 'DELETE':
try:
db.execute('DELETE FROM evaluations WHERE bid_id IN (SELECT id FROM bids WHERE project_id=?)', (pid,))
db.execute('DELETE FROM bids WHERE project_id=?', (pid,))
db.execute('DELETE FROM projects WHERE id=?', (pid,))
db.commit()
return jsonify({'message': '删除成功'})
except Exception as e:
db.rollback()
return jsonify({'error': str(e)}), 400
if request.method == 'PUT':
data = request.get_json(silent=True) or {}
try:
db.execute(
'''UPDATE projects SET name=?, code=?, category=?, budget=?, status=?,
publish_date=?, deadline=?, description=? WHERE id=?''',
(data.get('name', ''), data.get('code', ''), data.get('category', ''),
float(data.get('budget', 0)), data.get('status', 'open'),
data.get('publish_date', ''), data.get('deadline', ''),
data.get('description', ''), pid)
)
db.commit()
return jsonify({'message': '更新成功'})
except Exception as e:
db.rollback()
return jsonify({'error': str(e)}), 400
row = db.execute('SELECT * FROM projects WHERE id=?', (pid,)).fetchone()
if not row:
return jsonify({'error': '项目不存在'}), 404
return jsonify(dict(row))
@app.route('/api/bidders', methods=['GET', 'POST'])
def api_bidders():
db = g.db
if request.method == 'POST':
data = request.get_json(silent=True) or {}
if not data.get('name'):
return jsonify({'error': '缺少必填字段: name'}), 400
try:
cur = db.execute(
'''INSERT INTO bidders (name, company, contact, phone, qualification)
VALUES (?,?,?,?,?)''',
(data['name'], data.get('company', ''), data.get('contact', ''),
data.get('phone', ''), data.get('qualification', ''))
)
db.commit()
return jsonify({'id': cur.lastrowid, 'message': '创建成功'}), 201
except Exception as e:
db.rollback()
return jsonify({'error': str(e)}), 400
rows = db.execute('SELECT * FROM bidders ORDER BY name').fetchall()
return jsonify([dict(r) for r in rows])
@app.route('/api/bidders/<int:bid>', methods=['GET', 'PUT', 'DELETE'])
def api_bidder_detail(bid):
db = g.db
if request.method == 'DELETE':
try:
db.execute('DELETE FROM evaluations WHERE bid_id IN (SELECT id FROM bids WHERE bidder_id=?)', (bid,))
db.execute('DELETE FROM bids WHERE bidder_id=?', (bid,))
db.execute('DELETE FROM bidders WHERE id=?', (bid,))
db.commit()
return jsonify({'message': '删除成功'})
except Exception as e:
db.rollback()
return jsonify({'error': str(e)}), 400
if request.method == 'PUT':
data = request.get_json(silent=True) or {}
try:
db.execute(
'''UPDATE bidders SET name=?, company=?, contact=?, phone=?, qualification=? WHERE id=?''',
(data.get('name', ''), data.get('company', ''), data.get('contact', ''),
data.get('phone', ''), data.get('qualification', ''), bid)
)
db.commit()
return jsonify({'message': '更新成功'})
except Exception as e:
db.rollback()
return jsonify({'error': str(e)}), 400
row = db.execute('SELECT * FROM bidders WHERE id=?', (bid,)).fetchone()
if not row:
return jsonify({'error': '投标方不存在'}), 404
return jsonify(dict(row))
@app.route('/api/bids', methods=['GET', 'POST'])
def api_bids():
db = g.db
if request.method == 'POST':
data = request.get_json(silent=True) or {}
for k in ('project_id', 'bidder_id'):
if not data.get(k):
return jsonify({'error': f'缺少必填字段: {k}'}), 400
try:
cur = db.execute(
'''INSERT INTO bids (project_id, bidder_id, quote_price, quote_duration, status, remark)
VALUES (?,?,?,?,?,?)''',
(data['project_id'], data['bidder_id'],
float(data.get('quote_price', 0)), int(data.get('quote_duration', 0)),
data.get('status', 'pending'), data.get('remark', ''))
)
db.commit()
return jsonify({'id': cur.lastrowid, 'message': '投标提交成功'}), 201
except Exception as e:
db.rollback()
return jsonify({'error': str(e)}), 400
pid = request.args.get('project_id', type=int)
if pid:
rows = db.execute('SELECT * FROM bids WHERE project_id=? ORDER BY quote_price', (pid,)).fetchall()
else:
rows = db.execute('SELECT * FROM bids ORDER BY submit_date DESC').fetchall()
return jsonify([dict(r) for r in rows])
@app.route('/api/bids/<int:bid_id>', methods=['GET', 'PUT', 'DELETE'])
def api_bid_detail(bid_id):
db = g.db
if request.method == 'DELETE':
try:
db.execute('DELETE FROM evaluations WHERE bid_id=?', (bid_id,))
db.execute('DELETE FROM bids WHERE id=?', (bid_id,))
db.commit()
return jsonify({'message': '删除成功'})
except Exception as e:
db.rollback()
return jsonify({'error': str(e)}), 400
if request.method == 'PUT':
data = request.get_json(silent=True) or {}
try:
db.execute(
'''UPDATE bids SET project_id=?, bidder_id=?, quote_price=?, quote_duration=?, status=?, remark=? WHERE id=?''',
(data.get('project_id'), data.get('bidder_id'),
float(data.get('quote_price', 0)), int(data.get('quote_duration', 0)),
data.get('status', 'pending'), data.get('remark', ''), bid_id)
)
db.commit()
return jsonify({'message': '更新成功'})
except Exception as e:
db.rollback()
return jsonify({'error': str(e)}), 400
row = db.execute('SELECT * FROM bids WHERE id=?', (bid_id,)).fetchone()
if not row:
return jsonify({'error': '投标记录不存在'}), 404
return jsonify(dict(row))
@app.route('/api/bids/<int:bid_id>/evaluate', methods=['POST'])
def api_evaluate(bid_id):
db = g.db
data = request.get_json(silent=True) or {}
for k in ('technical_score', 'price_score', 'business_score'):
if k not in data:
return jsonify({'error': f'缺少必填字段: {k}'}), 400
total = (float(data['technical_score']) * 0.4 +
float(data['price_score']) * 0.3 +
float(data['business_score']) * 0.3)
try:
existing = db.execute('SELECT id FROM evaluations WHERE bid_id=?', (bid_id,)).fetchone()
if existing:
db.execute(
'''UPDATE evaluations SET technical_score=?, price_score=?, business_score=?, total_score=?, evaluator=?, comment=? WHERE bid_id=?''',
(float(data['technical_score']), float(data['price_score']),
float(data['business_score']), round(total, 1),
data.get('evaluator', ''), data.get('comment', ''), bid_id)
)
db.commit()
return jsonify({'total_score': round(total, 1), 'message': '评审更新成功'})
cur = db.execute(
'''INSERT INTO evaluations (bid_id, technical_score, price_score, business_score, total_score, evaluator, comment)
VALUES (?,?,?,?,?,?,?)''',
(bid_id, float(data['technical_score']), float(data['price_score']),
float(data['business_score']), round(total, 1),
data.get('evaluator', ''), data.get('comment', ''))
)
db.commit()
return jsonify({'id': cur.lastrowid, 'total_score': round(total, 1), 'message': '评审完成'}), 201
except Exception as e:
db.rollback()
return jsonify({'error': str(e)}), 400
@app.route('/admin')
def admin_dashboard():
db = g.db
stats = get_stats(db)
stats['total_bidders'] = db.execute('SELECT COUNT(*) as c FROM bidders').fetchone()['c']
stats['total_evaluations'] = db.execute('SELECT COUNT(*) as c FROM evaluations').fetchone()['c']
recent_projects = db.execute('SELECT * FROM projects ORDER BY created_at DESC LIMIT 5').fetchall()
recent_bids = db.execute(
'''SELECT b.*, p.name as project_name, bi.name as bidder_name
FROM bids b JOIN projects p ON b.project_id=p.id JOIN bidders bi ON b.bidder_id=bi.id
ORDER BY b.submit_date DESC LIMIT 5'''
).fetchall()
return render_template('admin/dashboard.html', stats=stats,
recent_projects=recent_projects, recent_bids=recent_bids)
@app.route('/admin/projects')
def admin_projects():
db = g.db
rows = db.execute('SELECT * FROM projects ORDER BY created_at DESC').fetchall()
return render_template('admin/projects.html', projects=[dict(r) for r in rows])
@app.route('/admin/bidders')
def admin_bidders():
db = g.db
rows = db.execute('SELECT * FROM bidders ORDER BY name').fetchall()
return render_template('admin/bidders.html', bidders=[dict(r) for r in rows])
@app.route('/admin/bids')
def admin_bids():
db = g.db
rows = db.execute(
'''SELECT b.*, p.name as project_name, p.code as project_code, bi.name as bidder_name
FROM bids b JOIN projects p ON b.project_id=p.id JOIN bidders bi ON b.bidder_id=bi.id
ORDER BY b.submit_date DESC'''
).fetchall()
projects = db.execute('SELECT id, name FROM projects ORDER BY name').fetchall()
bidders = db.execute('SELECT id, name FROM bidders ORDER BY name').fetchall()
return render_template('admin/bids.html', bids=[dict(r) for r in rows], projects=projects, bidders=bidders)
@app.route('/admin/evaluations')
def admin_evaluations():
db = g.db
rows = db.execute(
'''SELECT e.*, b.id as bid_id, p.name as project_name, bi.name as bidder_name, b.quote_price
FROM evaluations e JOIN bids b ON e.bid_id=b.id
JOIN projects p ON b.project_id=p.id JOIN bidders bi ON b.bidder_id=bi.id
ORDER BY e.eval_date DESC'''
).fetchall()
return render_template('admin/evaluations.html', evaluations=rows)
@app.errorhandler(404)
def not_found(e):
if request.path.startswith('/api/'):
return jsonify({'error': '资源不存在'}), 404
return render_template('404.html'), 404
@app.errorhandler(500)
def server_error(e):
if request.path.startswith('/api/'):
return jsonify({'error': '服务器内部错误'}), 500
return render_template('500.html'), 500
if __name__ == '__main__':
init_db()
port = int(os.environ.get('PORT', 5000))
debug = os.environ.get('FLASK_DEBUG', '0') == '1'
app.run(host='0.0.0.0', port=port, debug=debug)