Trae Assistant commited on
Commit
7afd68c
·
1 Parent(s): 4d00ccd

后端:新增 /api/upload,完善异常处理与中文文案;本地验证通过。

Browse files
Files changed (3) hide show
  1. __pycache__/app.cpython-314.pyc +0 -0
  2. app.py +97 -78
  3. requirements.txt +1 -0
__pycache__/app.cpython-314.pyc ADDED
Binary file (7.8 kB). View file
 
app.py CHANGED
@@ -2,37 +2,11 @@ from flask import Flask, render_template, request, jsonify, send_from_directory
2
  import numpy as np
3
  from scipy import stats
4
  import os
 
 
5
 
6
  app = Flask(__name__)
7
- # Security: Limit max upload size to 5MB
8
- app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024
9
-
10
- # Global Error Handlers
11
- @app.errorhandler(413)
12
- def request_entity_too_large(error):
13
- return jsonify({"error": "File too large (Max 5MB)"}), 413
14
-
15
- @app.errorhandler(500)
16
- def internal_error(error):
17
- return jsonify({"error": "Internal Server Error"}), 500
18
-
19
- @app.errorhandler(404)
20
- def not_found(error):
21
- return jsonify({"error": "Not Found"}), 404
22
-
23
- def validate_file(file_stream):
24
- """
25
- Robust file validation:
26
- 1. Check for null bytes (binary file protection)
27
- 2. Check file content validity (basic JSON structure)
28
- """
29
- chunk = file_stream.read(1024)
30
- file_stream.seek(0) # Reset pointer
31
-
32
- if b'\0' in chunk:
33
- return False, "File contains binary data (null bytes)"
34
-
35
- return True, ""
36
 
37
  @app.route('/')
38
  def index():
@@ -42,60 +16,19 @@ def index():
42
  def health():
43
  return "OK", 200
44
 
45
- @app.route('/api/upload', methods=['POST'])
46
- def upload_data():
47
- """
48
- Handle history data upload.
49
- Expects a JSON file with 'history' array.
50
- """
51
- if 'file' not in request.files:
52
- return jsonify({"error": "No file part"}), 400
53
-
54
- file = request.files['file']
55
- if file.filename == '':
56
- return jsonify({"error": "No selected file"}), 400
57
-
58
- if not file.filename.lower().endswith('.json'):
59
- return jsonify({"error": "Only JSON files are allowed"}), 400
60
-
61
- # Binary check
62
- is_valid, error_msg = validate_file(file.stream)
63
- if not is_valid:
64
- return jsonify({"error": error_msg}), 400
65
-
66
- try:
67
- import json
68
- data = json.load(file)
69
- if 'history' not in data or not isinstance(data['history'], list):
70
- return jsonify({"error": "Invalid JSON format: must contain 'history' list"}), 400
71
-
72
- # Validate history items
73
- valid_history = []
74
- for item in data['history']:
75
- if 'price' in item and 'sales' in item:
76
- valid_history.append(item)
77
-
78
- return jsonify({
79
- "status": "success",
80
- "message": f"Successfully loaded {len(valid_history)} records",
81
- "history": valid_history
82
- })
83
- except Exception as e:
84
- return jsonify({"error": f"Failed to parse JSON: {str(e)}"}), 500
85
-
86
  @app.route('/api/optimize', methods=['POST'])
87
  def optimize():
88
  """
89
- Analyzes sales data to estimate demand curve and recommend optimal price.
90
- Input: JSON { "history": [ {"price": 10, "sales": 50}, ... ], "mc": 5 }
91
- Output: JSON { "elasticity": -1.5, "optimal_price": 12.5, "model": "linear" }
92
  """
93
  data = request.json
94
  history = data.get('history', [])
95
  marginal_cost = data.get('mc', 0)
96
 
97
  if len(history) < 3:
98
- return jsonify({"error": "Need at least 3 data points"}), 400
99
 
100
  prices = np.array([h['price'] for h in history])
101
  sales = np.array([h['sales'] for h in history])
@@ -105,12 +38,11 @@ def optimize():
105
  slope, intercept, r_value, p_value, std_err = stats.linregress(prices, sales)
106
 
107
  if slope >= 0:
108
- # Abnormal demand (higher price -> higher sales?), fallback
109
  return jsonify({
110
  "status": "warning",
111
- "message": "Demand curve is inverted or flat (Slope >= 0). Cannot optimize.",
112
  "slope": slope,
113
- "optimal_price": prices[-1] # Keep current
114
  })
115
 
116
  # Profit = (P - MC) * Q = (P - MC) * (a + bP)
@@ -136,8 +68,95 @@ def optimize():
136
  "r_squared": r_value**2,
137
  "optimal_price": round(optimal_price, 2),
138
  "elasticity": round(elasticity, 2),
139
- "message": "Optimization successful based on linear demand model."
140
  })
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  if __name__ == '__main__':
143
  app.run(host='0.0.0.0', port=7860, debug=True)
 
2
  import numpy as np
3
  from scipy import stats
4
  import os
5
+ import json
6
+ from werkzeug.exceptions import RequestEntityTooLarge
7
 
8
  app = Flask(__name__)
9
+ app.config['MAX_CONTENT_LENGTH'] = 6 * 1024 * 1024
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  @app.route('/')
12
  def index():
 
16
  def health():
17
  return "OK", 200
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  @app.route('/api/optimize', methods=['POST'])
20
  def optimize():
21
  """
22
+ 基于历史价格-销量数据估计线性需求曲线并推荐最优价格。
23
+ 输入: JSON { "history": [ {"price": 10, "sales": 50}, ... ], "mc": 5 }
24
+ 输出: JSON { "elasticity": -1.5, "optimal_price": 12.5 }
25
  """
26
  data = request.json
27
  history = data.get('history', [])
28
  marginal_cost = data.get('mc', 0)
29
 
30
  if len(history) < 3:
31
+ return jsonify({"error": "至少需要 3 条数据点"}), 400
32
 
33
  prices = np.array([h['price'] for h in history])
34
  sales = np.array([h['sales'] for h in history])
 
38
  slope, intercept, r_value, p_value, std_err = stats.linregress(prices, sales)
39
 
40
  if slope >= 0:
 
41
  return jsonify({
42
  "status": "warning",
43
+ "message": "需求曲线异常或过于平坦(斜率≥0),无法优化,建议保持当前价格。",
44
  "slope": slope,
45
+ "optimal_price": prices[-1]
46
  })
47
 
48
  # Profit = (P - MC) * Q = (P - MC) * (a + bP)
 
68
  "r_squared": r_value**2,
69
  "optimal_price": round(optimal_price, 2),
70
  "elasticity": round(elasticity, 2),
71
+ "message": "线性需求模型优化成功,已生成建议价格。"
72
  })
73
 
74
+ @app.route('/api/upload', methods=['POST'])
75
+ def upload():
76
+ """
77
+ 上传并解析历史数据 JSON 文件,返回标准化的 history。
78
+ 支持两种格式:
79
+ 1) { "history": [ {price, sales, ...}, ... ] }
80
+ 2) [ {price, sales, ...}, ... ]
81
+ 约束:文件大小 ≤ 5MB,必须为 UTF-8 文本 JSON。
82
+ """
83
+ if 'file' not in request.files:
84
+ return jsonify({"error": "未找到文件字段"}), 400
85
+ file = request.files['file']
86
+ filename = (file.filename or '').lower()
87
+ if not filename.endswith('.json'):
88
+ return jsonify({"error": "只支持 JSON 文件"}), 400
89
+
90
+ # 读取并校验大小
91
+ file.seek(0, os.SEEK_END)
92
+ size = file.tell()
93
+ file.seek(0)
94
+ if size > 5 * 1024 * 1024:
95
+ return jsonify({"error": "文件大小不能超过 5MB"}), 400
96
+
97
+ try:
98
+ raw = file.read()
99
+ try:
100
+ text = raw.decode('utf-8')
101
+ except UnicodeDecodeError:
102
+ return jsonify({"error": "文件内容不是有效的 UTF-8 文本"}), 400
103
+
104
+ payload = json.loads(text)
105
+ if isinstance(payload, dict) and 'history' in payload:
106
+ incoming = payload['history']
107
+ elif isinstance(payload, list):
108
+ incoming = payload
109
+ else:
110
+ return jsonify({"error": "JSON 格式不正确,应为 {history: [...]} 或数组"}), 400
111
+
112
+ cleaned = []
113
+ day_counter = 0
114
+ for item in incoming:
115
+ try:
116
+ price = float(item.get('price'))
117
+ sales = int(item.get('sales'))
118
+ except Exception:
119
+ # 跳过不可解析的数据行
120
+ continue
121
+ if price <= 0 or sales < 0:
122
+ continue
123
+ day_counter += 1
124
+ revenue = item.get('revenue')
125
+ if revenue is None:
126
+ revenue = price * sales
127
+ competitor_price = item.get('competitorPrice', 50)
128
+ cleaned.append({
129
+ "day": item.get('day', day_counter),
130
+ "price": price,
131
+ "sales": sales,
132
+ "revenue": revenue,
133
+ "competitorPrice": competitor_price
134
+ })
135
+
136
+ if not cleaned:
137
+ return jsonify({"error": "数据为空或格式不正确"}), 400
138
+
139
+ return jsonify({"history": cleaned})
140
+ except json.JSONDecodeError:
141
+ return jsonify({"error": "JSON 解析失败,请检查文件内容"}), 400
142
+ except Exception as e:
143
+ return jsonify({"error": f"服务器解析失败: {str(e)}"}), 500
144
+
145
+ @app.errorhandler(404)
146
+ def handle_404(e):
147
+ if request.path.startswith('/api/'):
148
+ return jsonify({"error": "未找到接口", "path": request.path}), 404
149
+ return render_template('index.html'), 404
150
+
151
+ @app.errorhandler(500)
152
+ def handle_500(e):
153
+ if request.path.startswith('/api/'):
154
+ return jsonify({"error": "服务器内部错误"}), 500
155
+ return render_template('index.html'), 500
156
+
157
+ @app.errorhandler(RequestEntityTooLarge)
158
+ def handle_file_too_large(e):
159
+ return jsonify({"error": "文件过大,服务端限制为 6MB"}), 413
160
+
161
  if __name__ == '__main__':
162
  app.run(host='0.0.0.0', port=7860, debug=True)
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  Flask
2
  numpy
3
  scipy
 
4
  gunicorn
 
1
  Flask
2
  numpy
3
  scipy
4
+ pandas
5
  gunicorn