初始提交:人事共享服务中心钉钉登录功能
This commit is contained in:
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
*.cache
|
||||
|
||||
# Backup files
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.rar
|
||||
148
README.md
Normal file
148
README.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 人事共享服务中心"码"上办 - 需求收集系统
|
||||
|
||||
## Linux服务器部署指南
|
||||
|
||||
### 1. 上传并解压包
|
||||
|
||||
```bash
|
||||
# 上传 requirement-collection.tar.gz 到服务器
|
||||
|
||||
# 解压
|
||||
tar -xzf requirement-collection.tar.gz
|
||||
cd mashangban
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 初始化数据库
|
||||
|
||||
```bash
|
||||
python3 init_db.py
|
||||
```
|
||||
|
||||
### 4. 启动服务
|
||||
|
||||
```bash
|
||||
python3 run.py
|
||||
```
|
||||
|
||||
### 5. 访问地址
|
||||
|
||||
- 基础地址:http://your-server-ip:5001/requirement-collection
|
||||
- 带钉钉参数:http://your-server-ip:5001/requirement-collection/?userId={{发起人userid}}&name={{发起人姓名}}&dept={{发起人部门}}
|
||||
|
||||
### 6. 钉钉集成
|
||||
|
||||
在钉钉中配置机器人或应用,传入以下参数:
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| userId | 钉钉用户ID | manager123 |
|
||||
| name | 用户姓名 | 张三 |
|
||||
| dept | 部门名称 | 人事部 |
|
||||
|
||||
**示例URL:**
|
||||
```
|
||||
https://askill.top/requirement-collection/?userId=user001&name=张三&dept=人事部
|
||||
```
|
||||
|
||||
**首次访问:** 系统自动创建用户并记录姓名和部门信息
|
||||
|
||||
**后续访问:** 根据userId自动识别用户并登录
|
||||
|
||||
### 7. 管理员配置
|
||||
|
||||
默认管理员账户信息:
|
||||
- 用户名:管理员
|
||||
- dingtalk_userid:admin
|
||||
|
||||
如需修改为其他管理员,需要手动编辑数据库。
|
||||
|
||||
### 8. 修改配置(可选)
|
||||
|
||||
#### 修改端口
|
||||
编辑 `run.py` 中的端口号:
|
||||
```python
|
||||
run_simple('0.0.0.0', 端口号, application, ...)
|
||||
```
|
||||
|
||||
#### 使用Gunicorn部署(生产环境推荐)
|
||||
|
||||
```bash
|
||||
pip3 install gunicorn
|
||||
gunicorn -w 4 -b 0.0.0.0:5001 'run:application'
|
||||
```
|
||||
|
||||
#### Nginx反向代理配置
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location /requirement-collection {
|
||||
proxy_pass http://127.0.0.1:5001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9. 后台运行
|
||||
|
||||
使用systemd管理服务:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/requirement-collection.service
|
||||
```
|
||||
|
||||
写入以下内容:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Requirement Collection Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
WorkingDirectory=/path/to/mashangban
|
||||
ExecStart=/usr/bin/python3 /path/to/mashangban/run.py
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启用服务:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable requirement-collection
|
||||
sudo systemctl start requirement-collection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
|
||||
- **需求汇总**:公开页面,显示所有公开需求
|
||||
- **提交新需求**:登录后可提交(需求标题、需求内容、分会、联系方式、是否公开)
|
||||
- **我的需求**:查看和管理自己提交的需求
|
||||
- **管理后台**:管理员可回答需求、修改公开状态
|
||||
|
||||
## 权限说明
|
||||
|
||||
| 功能 | 普通用户 | 管理员 |
|
||||
|------|---------|--------|
|
||||
| 查看公开需求 | ✓ | ✓ |
|
||||
| 提交需求 | ✓ | ✓ |
|
||||
| 编辑自己的需求 | ✓ | ✓ |
|
||||
| 回答需求 | - | ✓ |
|
||||
| 强制修改公开状态 | - | ✓ |
|
||||
| 查看所有需求 | - | ✓ |
|
||||
44
__init__.py
Normal file
44
__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
from flask import Flask, request
|
||||
from flask_login import LoginManager
|
||||
from config import Config
|
||||
from models import db, User
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
|
||||
dingtalk_app_key = os.environ.get('DINGTALK_APP_KEY', '')
|
||||
dingtalk_app_secret = os.environ.get('DINGTALK_APP_SECRET', '')
|
||||
dingtalk_agent_id = os.environ.get('DINGTALK_AGENT_ID', '')
|
||||
dingtalk_corp_id = os.environ.get('DINGTALK_CORP_ID', '')
|
||||
|
||||
app.config['DINGTALK_APP_KEY'] = dingtalk_app_key
|
||||
app.config['DINGTALK_APP_SECRET'] = dingtalk_app_secret
|
||||
app.config['DINGTALK_AGENT_ID'] = dingtalk_agent_id
|
||||
app.config['DINGTALK_CORP_ID'] = dingtalk_corp_id
|
||||
app.config['DINGTALK_TARGET_URL'] = os.environ.get('DINGTALK_TARGET_URL', 'http://localhost:5001/requirement-collection')
|
||||
app.config['APPLICATION_ROOT'] = '/requirement-collection'
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'index'
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
from routes import *
|
||||
from dingtalk import dingtalk_bp
|
||||
|
||||
app.register_blueprint(dingtalk_bp, url_prefix='/dingtalk')
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
6
config.py
Normal file
6
config.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard_to_guess_string_for_flask_app'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///question_collector.db'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
292
dingtalk.py
Normal file
292
dingtalk.py
Normal file
@@ -0,0 +1,292 @@
|
||||
import time
|
||||
import hashlib
|
||||
import requests
|
||||
from flask import Blueprint, render_template, jsonify, current_app, request
|
||||
|
||||
dingtalk_bp = Blueprint('dingtalk', __name__)
|
||||
|
||||
def get_dingtalk_config():
|
||||
print('[后端] [1] ============== get_dingtalk_config 开始 ==============')
|
||||
app_key = current_app.config.get('DINGTALK_APP_KEY', '')
|
||||
agent_id = current_app.config.get('DINGTALK_AGENT_ID', '')
|
||||
corp_id = current_app.config.get('DINGTALK_CORP_ID', '')
|
||||
app_secret = current_app.config.get('DINGTALK_APP_SECRET', '')
|
||||
target_url = current_app.config.get('DINGTALK_TARGET_URL', 'http://localhost:5001/requirement-collection')
|
||||
|
||||
print(f'[后端] [1] 配置读取: app_key={app_key}, agent_id={agent_id}, corp_id={corp_id}')
|
||||
|
||||
if not app_key or not app_secret:
|
||||
print(f'[后端] [1] 缺少 app_key 或 app_secret, 返回 None')
|
||||
return None
|
||||
|
||||
result = {
|
||||
'app_key': app_key,
|
||||
'app_secret': app_secret,
|
||||
'agent_id': agent_id,
|
||||
'corp_id': corp_id,
|
||||
'target_url': target_url
|
||||
}
|
||||
print(f'[后端] [1] get_dingtalk_config 完成, 返回:', result)
|
||||
return result
|
||||
|
||||
def get_access_token():
|
||||
print('[后端] [2] ============== get_access_token 开始 ==============')
|
||||
app_key = current_app.config.get('DINGTALK_APP_KEY')
|
||||
app_secret = current_app.config.get('DINGTALK_APP_SECRET')
|
||||
|
||||
if not app_key or not app_secret:
|
||||
print('[后端] [2] 缺少 app_key 或 app_secret, 返回 None')
|
||||
return None
|
||||
|
||||
url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
data = {'appKey': app_key, 'appSecret': app_secret}
|
||||
print(f'[后端] [2] 请求钉钉API: {url}, data: appKey={app_key}')
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=data, headers=headers, timeout=10)
|
||||
result = response.json()
|
||||
print(f'[后端] [2] 钉钉响应:', result)
|
||||
if result.get('accessToken'):
|
||||
print(f'[后端] [2] 获取access_token成功')
|
||||
return result['accessToken']
|
||||
except Exception as e:
|
||||
print(f'[后端] [2] 异常:', e)
|
||||
return None
|
||||
|
||||
def get_jsapi_ticket():
|
||||
print('[后端] [2b] ============== get_jsapi_ticket 开始 ==============')
|
||||
app_key = current_app.config.get('DINGTALK_APP_KEY')
|
||||
app_secret = current_app.config.get('DINGTALK_APP_SECRET')
|
||||
|
||||
if not app_key or not app_secret:
|
||||
print('[后端] [2b] 缺少 app_key 或 app_secret, 返回 None')
|
||||
return None
|
||||
|
||||
url = f'https://oapi.dingtalk.com/gettoken?appkey={app_key}&appsecret={app_secret}'
|
||||
print(f'[后端] [2b] 请求钉钉API: {url}')
|
||||
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
result = response.json()
|
||||
print(f'[后端] [2b] 获取token响应:', result)
|
||||
if result.get('access_token'):
|
||||
access_token = result['access_token']
|
||||
ticket_url = f'https://oapi.dingtalk.com/get_jsapi_ticket?access_token={access_token}'
|
||||
print(f'[后端] [2b] 请求ticket API: {ticket_url}')
|
||||
ticket_response = requests.get(ticket_url, timeout=10)
|
||||
ticket_result = ticket_response.json()
|
||||
print(f'[后端] [2b] 获取ticket响应:', ticket_result)
|
||||
if ticket_result.get('ticket'):
|
||||
print(f'[后端] [2b] 获取jsapi_ticket成功')
|
||||
return ticket_result['ticket']
|
||||
except Exception as e:
|
||||
print(f'[后端] [2b] 异常:', e)
|
||||
return None
|
||||
|
||||
def generate_signature(jsapi_ticket, nonce_str, timestamp, url):
|
||||
print(f'[后端] [2c] ============== generate_signature 开始 ==============')
|
||||
print(f'[后端] [2c] 参数: jsapi_ticket={jsapi_ticket[:20] if jsapi_ticket else "None"}..., nonce_str={nonce_str}, timestamp={timestamp}, url={url}')
|
||||
|
||||
sign_str = f'jsapi_ticket={jsapi_ticket}&noncestr={nonce_str}×tamp={timestamp}&url={url}'
|
||||
print(f'[后端] [2c] 签名字符串: {sign_str}')
|
||||
|
||||
signature = hashlib.sha1(sign_str.encode('utf-8')).hexdigest()
|
||||
print(f'[后端] [2c] 签名结果: {signature}')
|
||||
return signature
|
||||
|
||||
def get_user_info_by_code(code):
|
||||
print(f'[后端] [3] ============== get_user_info_by_code 开始, code={code} ==============')
|
||||
access_token = get_access_token()
|
||||
if not access_token:
|
||||
print('[后端] [3] 获取 access_token 失败, 返回 None')
|
||||
return None
|
||||
|
||||
# 使用钉钉微应用API通过临时授权码获取用户信息
|
||||
url = f'https://oapi.dingtalk.com/topapi/v2/user/getuserinfo?access_token={access_token}'
|
||||
print(f'[后端] [3] 请求钉钉API: {url}')
|
||||
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
data = {'code': code}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=data, headers=headers, timeout=10)
|
||||
result = response.json()
|
||||
print(f'[后端] [3] 钉钉用户信息响应:', result)
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f'[后端] [3] 异常:', e)
|
||||
return None
|
||||
|
||||
def get_user_detail(user_id):
|
||||
print(f'[后端] [4] ============== get_user_detail 开始, user_id={user_id} ==============')
|
||||
access_token = get_access_token()
|
||||
if not access_token:
|
||||
print('[后端] [4] 获取 access_token 失败, 返回 None')
|
||||
return None
|
||||
|
||||
# 使用新版API获取用户详情
|
||||
url = f'https://oapi.dingtalk.com/topapi/v2/user/get?access_token={access_token}'
|
||||
print(f'[后端] [4] 请求钉钉API: {url}')
|
||||
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
data = {'userid': user_id}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=data, headers=headers, timeout=10)
|
||||
result = response.json()
|
||||
print(f'[后端] [4] 钉钉用户详情响应:', result)
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f'[后端] [4] 异常:', e)
|
||||
return None
|
||||
|
||||
def get_dept_name(dept_id):
|
||||
print(f'[后端] [5] ============== get_dept_name 开始, dept_id={dept_id} ==============')
|
||||
access_token = get_access_token()
|
||||
if not access_token:
|
||||
print('[后端] [5] 获取 access_token 失败, 返回 None')
|
||||
return None
|
||||
|
||||
# 使用新版API获取部门信息
|
||||
url = f'https://oapi.dingtalk.com/topapi/v2/department/get?access_token={access_token}'
|
||||
print(f'[后端] [5] 请求钉钉API: {url}')
|
||||
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
data = {'dept_id': int(dept_id)}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=data, headers=headers, timeout=10)
|
||||
result = response.json()
|
||||
print(f'[后端] [5] 钉钉部门信息响应:', result)
|
||||
if result.get('errcode') == 0 and result.get('result') and result['result'].get('name'):
|
||||
print(f'[后端] [5] 获取部门名称成功: {result["result"]["name"]}')
|
||||
return result['result']['name']
|
||||
except Exception as e:
|
||||
print(f'[后端] [5] 异常:', e)
|
||||
return None
|
||||
|
||||
@dingtalk_bp.route('/')
|
||||
def dingtalk_entry():
|
||||
print('[后端] [0] ============== dingtalk_entry 请求到达 ==============')
|
||||
|
||||
config = get_dingtalk_config()
|
||||
|
||||
if not config:
|
||||
print('[后端] [0] 配置为空,使用默认值')
|
||||
return render_template('dingtalk_entry.html',
|
||||
app_key='',
|
||||
agent_id='',
|
||||
corp_id='',
|
||||
target_url='http://localhost:5001/requirement-collection'
|
||||
), 400
|
||||
|
||||
print('[后端] [0] 渲染 dingtalk_entry.html')
|
||||
return render_template('dingtalk_entry.html',
|
||||
app_key=config['app_key'],
|
||||
agent_id=config['agent_id'],
|
||||
corp_id=config['corp_id'],
|
||||
target_url=config['target_url']
|
||||
)
|
||||
|
||||
@dingtalk_bp.route('/api/getSignature')
|
||||
def get_signature():
|
||||
print('[后端] [S] ============== get_signature 请求到达 ==============')
|
||||
url = request.args.get('url', '')
|
||||
print(f'[后端] [S] 收到前端传来的URL: {url}')
|
||||
|
||||
if not url:
|
||||
print('[后端] [S] 缺少url参数')
|
||||
return jsonify({'error': '缺少url参数'}), 400
|
||||
|
||||
config = get_dingtalk_config()
|
||||
if not config:
|
||||
print('[后端] [S] 配置为空')
|
||||
return jsonify({'error': '配置为空'}), 400
|
||||
|
||||
jsapi_ticket = get_jsapi_ticket()
|
||||
if not jsapi_ticket:
|
||||
print('[后端] [S] 获取jsapi_ticket失败')
|
||||
return jsonify({'error': '获取jsapi_ticket失败'}), 500
|
||||
|
||||
nonce_str = 'dingtalk' + str(int(time.time()))
|
||||
timestamp = str(int(time.time() * 1000))
|
||||
|
||||
signature = generate_signature(jsapi_ticket, nonce_str, timestamp, url)
|
||||
print(f'[后端] [S] ============== get_signature 完成 ==============')
|
||||
|
||||
return jsonify({
|
||||
'signature': signature,
|
||||
'nonceStr': nonce_str,
|
||||
'timeStamp': timestamp,
|
||||
'agentId': config['agent_id'] and int(config['agent_id']) or 0
|
||||
})
|
||||
|
||||
@dingtalk_bp.route('/api/getDingUser')
|
||||
def get_ding_user():
|
||||
print('[后端] [6] ============== get_ding_user 请求到达 ==============')
|
||||
code = request.args.get('code')
|
||||
print(f'[后端] [6] 收到 code 参数: {code}')
|
||||
|
||||
if not code:
|
||||
print('[后端] [6] 缺少 code 参数')
|
||||
return jsonify({'errcode': 400, 'errmsg': '缺少code参数'})
|
||||
|
||||
print('[后端] [6] 开始调用 get_user_info_by_code')
|
||||
user_info = get_user_info_by_code(code)
|
||||
if not user_info:
|
||||
print('[后端] [6] 获取用户信息失败')
|
||||
return jsonify({'errcode': 500, 'errmsg': '获取用户信息失败'})
|
||||
|
||||
# 处理新的API响应格式
|
||||
result = {
|
||||
'errcode': 0,
|
||||
'errmsg': 'ok',
|
||||
'userid': '',
|
||||
'name': '',
|
||||
'department': []
|
||||
}
|
||||
|
||||
if user_info.get('errcode') == 0 and user_info.get('result'):
|
||||
result['userid'] = user_info['result'].get('userid', '')
|
||||
else:
|
||||
# 兼容旧格式
|
||||
result['userid'] = user_info.get('userId', user_info.get('userid', ''))
|
||||
|
||||
print(f'[后端] [6] 初步结果: {result}')
|
||||
|
||||
if result['userid']:
|
||||
print('[后端] [6] 有 userid, 调用 get_user_detail 获取详情')
|
||||
user_detail = get_user_detail(result['userid'])
|
||||
if user_detail and user_detail.get('errcode') == 0 and user_detail.get('result'):
|
||||
result['name'] = user_detail['result'].get('name', '')
|
||||
dept_ids = user_detail['result'].get('dept_id_list', [])
|
||||
print(f'[后端] [6] 更新用户名为: {result["name"]}')
|
||||
print(f'[后端] [6] 部门ID列表: {dept_ids}')
|
||||
|
||||
# 获取部门名称
|
||||
if dept_ids:
|
||||
print('[后端] [6] 开始获取部门名称')
|
||||
dept_names = []
|
||||
for dept_id in dept_ids:
|
||||
name = get_dept_name(str(dept_id))
|
||||
if name:
|
||||
dept_names.append(name)
|
||||
if dept_names:
|
||||
result['department'] = dept_names
|
||||
print(f'[后端] [6] 更新部门为: {result["department"]}')
|
||||
elif user_detail:
|
||||
# 兼容旧格式
|
||||
result['name'] = user_detail.get('name', '')
|
||||
dept_ids = user_detail.get('department', [])
|
||||
if dept_ids:
|
||||
dept_names = []
|
||||
for dept_id in dept_ids:
|
||||
name = get_dept_name(str(dept_id))
|
||||
if name:
|
||||
dept_names.append(name)
|
||||
if dept_names:
|
||||
result['department'] = dept_names
|
||||
|
||||
print(f'[后端] [6] ============== get_ding_user 完成, 返回: {result} ==============')
|
||||
return jsonify(result)
|
||||
23
forms.py
Normal file
23
forms.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, TextAreaField, SelectField, BooleanField, SubmitField
|
||||
from wtforms.validators import DataRequired, Length
|
||||
|
||||
class DemandForm(FlaskForm):
|
||||
title = StringField('需求标题', validators=[DataRequired(), Length(max=200)])
|
||||
content = TextAreaField('需求内容', validators=[DataRequired(), Length(max=1000)])
|
||||
branch = SelectField('分会', choices=[
|
||||
('comprehensive', '综合分会'),
|
||||
('training', '培训服务分会'),
|
||||
('hr', '基础人事服务分会'),
|
||||
('talent', '人才服务分会'),
|
||||
('functional', '职能支持分会'),
|
||||
('finance_review', '经费审查委员会'),
|
||||
('women', '女职工委员会')
|
||||
], validators=[DataRequired()])
|
||||
contact = StringField('联系方式', validators=[DataRequired(), Length(max=100)])
|
||||
is_public = BooleanField('是否公开')
|
||||
submit = SubmitField('提交')
|
||||
|
||||
class AnswerForm(FlaskForm):
|
||||
answer = TextAreaField('回答内容', validators=[DataRequired(), Length(max=2000)])
|
||||
submit = SubmitField('保存回答')
|
||||
19
init_db.py
Normal file
19
init_db.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __init__ import app, db
|
||||
from models import User
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
if not User.query.filter_by(username='管理员').first():
|
||||
admin = User(
|
||||
username='管理员',
|
||||
dingtalk_userid='admin',
|
||||
dingtalk_name='管理员',
|
||||
dingtalk_dept='',
|
||||
role='admin'
|
||||
)
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
print('数据库初始化完成,已创建管理员账户')
|
||||
else:
|
||||
print('数据库已存在')
|
||||
38
models.py
Normal file
38
models.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from datetime import datetime
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import UserMixin
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
role = db.Column(db.String(20), nullable=False, default='user')
|
||||
contact = db.Column(db.String(100))
|
||||
dingtalk_userid = db.Column(db.String(100), unique=True, nullable=False)
|
||||
dingtalk_name = db.Column(db.String(100))
|
||||
dingtalk_dept = db.Column(db.String(200))
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def is_admin(self):
|
||||
return self.role == 'admin'
|
||||
|
||||
class Demand(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
branch = db.Column(db.String(50), nullable=False)
|
||||
contact = db.Column(db.String(100), nullable=False)
|
||||
is_public = db.Column(db.Boolean, default=True)
|
||||
answer = db.Column(db.Text)
|
||||
answered_at = db.Column(db.DateTime)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
user = db.relationship('User', backref=db.backref('demands', lazy=True))
|
||||
|
||||
def can_edit(self, current_user):
|
||||
if current_user.is_admin():
|
||||
return True
|
||||
return self.user_id == current_user.id
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Flask==3.0.0
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
Flask-WTF==1.2.1
|
||||
Flask-Login==0.6.3
|
||||
requests==2.31.0
|
||||
python-dotenv==1.0.0
|
||||
148
routes.py
Normal file
148
routes.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from datetime import datetime
|
||||
from flask import render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from __init__ import app, db
|
||||
from models import User, Demand
|
||||
from forms import DemandForm, AnswerForm
|
||||
|
||||
BRANCH_NAMES = {
|
||||
'comprehensive': '综合分会',
|
||||
'training': '培训服务分会',
|
||||
'hr': '基础人事服务分会',
|
||||
'talent': '人才服务分会',
|
||||
'functional': '职能支持分会',
|
||||
'finance_review': '经费审查委员会',
|
||||
'women': '女职工委员会'
|
||||
}
|
||||
|
||||
@app.context_processor
|
||||
def utility_processor():
|
||||
def get_branch_name(branch_key):
|
||||
return BRANCH_NAMES.get(branch_key, branch_key)
|
||||
return dict(get_branch_name=get_branch_name)
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
user_id = request.args.get('userId')
|
||||
name = request.args.get('name')
|
||||
dept = request.args.get('dept')
|
||||
|
||||
if user_id or name or dept:
|
||||
print(f'[后端] [7] ============== before_request 收到登录参数 ==============')
|
||||
print(f'[后端] [7] userId={user_id}, name={name}, dept={dept}')
|
||||
|
||||
if user_id and not current_user.is_authenticated:
|
||||
print(f'[后端] [7] 开始登录流程, userId={user_id}')
|
||||
user = User.query.filter_by(dingtalk_userid=user_id).first()
|
||||
|
||||
if user:
|
||||
print(f'[后端] [7] 用户已存在, 直接登录: {user.username}')
|
||||
login_user(user)
|
||||
elif name:
|
||||
print(f'[后端] [7] 用户不存在, 创建新用户: {name}')
|
||||
role = 'admin' if user_id == 'admin' else 'user'
|
||||
user = User(
|
||||
username=name,
|
||||
dingtalk_userid=user_id,
|
||||
dingtalk_name=name,
|
||||
dingtalk_dept=dept or '',
|
||||
role=role
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
login_user(user)
|
||||
print(f'[后端] [7] 用户创建并登录成功')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
demands = Demand.query.filter_by(is_public=True).order_by(Demand.created_at.desc()).all()
|
||||
return render_template('index.html', demands=demands)
|
||||
|
||||
@app.route('/demand/new', methods=['GET', 'POST'])
|
||||
def new_demand():
|
||||
if not current_user.is_authenticated:
|
||||
return render_template('not_logged_in.html')
|
||||
form = DemandForm()
|
||||
if form.validate_on_submit():
|
||||
demand = Demand(
|
||||
title=form.title.data,
|
||||
content=form.content.data,
|
||||
branch=form.branch.data,
|
||||
contact=form.contact.data,
|
||||
is_public=form.is_public.data,
|
||||
user_id=current_user.id
|
||||
)
|
||||
db.session.add(demand)
|
||||
db.session.commit()
|
||||
flash('需求提交成功')
|
||||
return redirect(url_for('index'))
|
||||
return render_template('demand_form.html', form=form, title='提交新需求')
|
||||
|
||||
@app.route('/demand/<int:id>/edit', methods=['GET', 'POST'])
|
||||
def edit_demand(id):
|
||||
if not current_user.is_authenticated:
|
||||
return render_template('not_logged_in.html')
|
||||
demand = Demand.query.get_or_404(id)
|
||||
if not demand.can_edit(current_user):
|
||||
flash('无权限编辑此需求')
|
||||
return redirect(url_for('index'))
|
||||
form = DemandForm(obj=demand)
|
||||
if form.validate_on_submit():
|
||||
demand.title = form.title.data
|
||||
demand.content = form.content.data
|
||||
demand.branch = form.branch.data
|
||||
demand.contact = form.contact.data
|
||||
if current_user.is_admin() or not demand.answer:
|
||||
demand.is_public = form.is_public.data
|
||||
demand.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
flash('需求更新成功')
|
||||
return redirect(url_for('index'))
|
||||
return render_template('demand_form.html', form=form, title='编辑需求', demand=demand)
|
||||
|
||||
@app.route('/demand/<int:id>/answer', methods=['GET', 'POST'])
|
||||
def answer_demand(id):
|
||||
if not current_user.is_authenticated:
|
||||
return render_template('not_logged_in.html')
|
||||
demand = Demand.query.get_or_404(id)
|
||||
if not current_user.is_admin():
|
||||
flash('只有管理员可以回答需求')
|
||||
return redirect(url_for('index'))
|
||||
form = AnswerForm(data={'answer': demand.answer or ''})
|
||||
if form.validate_on_submit():
|
||||
demand.answer = form.answer.data
|
||||
demand.answered_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
flash('回答已保存')
|
||||
return redirect(url_for('index'))
|
||||
return render_template('answer_form.html', form=form, demand=demand)
|
||||
|
||||
@app.route('/demand/<int:id>/toggle_public', methods=['POST'])
|
||||
def toggle_public(id):
|
||||
if not current_user.is_authenticated:
|
||||
return render_template('not_logged_in.html')
|
||||
demand = Demand.query.get_or_404(id)
|
||||
if not current_user.is_admin():
|
||||
flash('只有管理员可以修改公开状态')
|
||||
return redirect(url_for('index'))
|
||||
demand.is_public = not demand.is_public
|
||||
db.session.commit()
|
||||
flash('公开状态已更新')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/my_demands')
|
||||
def my_demands():
|
||||
if not current_user.is_authenticated:
|
||||
return render_template('not_logged_in.html')
|
||||
demands = Demand.query.filter_by(user_id=current_user.id).order_by(Demand.created_at.desc()).all()
|
||||
return render_template('my_demands.html', demands=demands)
|
||||
|
||||
@app.route('/admin/demands')
|
||||
def admin_demands():
|
||||
if not current_user.is_authenticated:
|
||||
return render_template('not_logged_in.html')
|
||||
if not current_user.is_admin():
|
||||
flash('无权限访问此页面')
|
||||
return redirect(url_for('index'))
|
||||
demands = Demand.query.order_by(Demand.created_at.desc()).all()
|
||||
return render_template('admin_demands.html', demands=demands)
|
||||
10
run.py
Normal file
10
run.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||
from __init__ import app
|
||||
|
||||
application = DispatcherMiddleware(app, {
|
||||
'/requirement-collection': app
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
from werkzeug.serving import run_simple
|
||||
run_simple('0.0.0.0', 5001, application, use_reloader=True, use_debugger=True)
|
||||
21
start.sh
Executable file
21
start.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "正在启动人事共享服务中心'码'上办..."
|
||||
|
||||
# 检查Python环境
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "错误: 未找到Python3,请先安装Python"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装依赖
|
||||
echo "检查依赖..."
|
||||
pip3 install -r requirements.txt --quiet
|
||||
|
||||
# 初始化数据库
|
||||
echo "初始化数据库..."
|
||||
python3 init_db.py
|
||||
|
||||
# 启动服务
|
||||
echo "启动服务..."
|
||||
python3 run.py
|
||||
33
start_dingtalk.sh
Normal file
33
start_dingtalk.sh
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "正在启动人事共享服务中心'码'上办..."
|
||||
|
||||
# 检查Python环境
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "错误: 未找到Python3,请先安装Python"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装依赖
|
||||
echo "检查依赖..."
|
||||
pip3 install -r requirements.txt --quiet
|
||||
|
||||
# 初始化数据库
|
||||
echo "初始化数据库..."
|
||||
python3 init_db.py
|
||||
|
||||
# 设置钉钉配置(请根据实际情况修改)
|
||||
export DINGTALK_APP_KEY="dingx1oxerkw6vfzwzsc"
|
||||
export DINGTALK_APP_SECRET="_Ap2DwtSm2nyuZdid8DwFdAc4rLWuXYCbSBTJrwuwbLdyFcPtNabrs4lmnBDdoz5"
|
||||
export DINGTALK_AGENT_ID="4583595778"
|
||||
export DINGTALK_CORP_ID="dingaac5c6bd7188ca3935c2f4657eb6378f"
|
||||
export DINGTALK_TARGET_URL="https://www.askill.top/requirement-collection"
|
||||
|
||||
# 启动服务
|
||||
echo "启动服务..."
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "钉钉入口地址: http://your-domain/requirement-collection/dingtalk/"
|
||||
echo "需求收集地址: http://your-domain/requirement-collection/"
|
||||
echo "========================================="
|
||||
python3 run.py
|
||||
BIN
static/logo.png
Normal file
BIN
static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
472
static/style.css
Normal file
472
static/style.css
Normal file
@@ -0,0 +1,472 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
padding: 0 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #4a90d9;
|
||||
color: white;
|
||||
padding: 15px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
header h1 a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
header nav ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
header nav ul li a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
header nav ul li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 20px 0;
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.flash {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.form {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.form-group-inline {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.form-group-inline label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.checkbox-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.checkbox-inline input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-inline label {
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #4a90d9;
|
||||
}
|
||||
|
||||
.form-control.textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
height: 150px;
|
||||
max-height: 200px;
|
||||
min-height: 120px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4a90d9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #3a80c9;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background-color: #f39c12;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background-color: #e67e22;
|
||||
}
|
||||
|
||||
.btn-answer {
|
||||
background-color: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-answer:hover {
|
||||
background-color: #2ecc71;
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
background-color: #95a5a6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-toggle:hover {
|
||||
background-color: #7f8c8d;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #ecf0f1;
|
||||
color: #333;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background-color: #bdc3c7;
|
||||
}
|
||||
|
||||
.question-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.question-item {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.question-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.branch {
|
||||
background-color: #4a90d9;
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status.public {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status.private {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status.waiting {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.user {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.demand-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 15px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.question-footer {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.contact {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.answer {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.answer p {
|
||||
margin: 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.answer-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.answer-toggle {
|
||||
cursor: pointer;
|
||||
color: #4a90d9;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.answer-toggle:hover {
|
||||
color: #3a80c9;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
margin-right: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.login-required {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-required h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.login-required p {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.login-hint p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.url-example {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
color: #4a90d9;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: #fff3cd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.question-preview {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.question-preview h3 {
|
||||
margin-bottom: 10px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.question-preview .meta {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #2c3e50;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
max-width: 960px;
|
||||
}
|
||||
}
|
||||
48
templates/admin_demands.html
Normal file
48
templates/admin_demands.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}管理后台{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>管理后台 - 所有需求</h2>
|
||||
{% if demands %}
|
||||
<div class="question-list">
|
||||
{% for demand in demands %}
|
||||
<div class="question-item">
|
||||
<div class="question-header">
|
||||
<span class="branch">{{ get_branch_name(demand.branch) }}</span>
|
||||
<span class="time">{{ demand.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
<span class="status {% if demand.is_public %}public{% else %}private{% endif %}">
|
||||
{% if demand.is_public %}公开{% else %}私有{% endif %}
|
||||
</span>
|
||||
<span class="user">提交者: {{ demand.user.username }}</span>
|
||||
</div>
|
||||
<h3 class="demand-title">{{ demand.title }}</h3>
|
||||
<p class="content">{{ demand.content }}</p>
|
||||
<div class="question-footer">
|
||||
<span class="contact">联系方式: {{ demand.contact }}</span>
|
||||
{% if demand.answer %}
|
||||
<div class="answer">
|
||||
<strong>回答:</strong>
|
||||
<p>{{ demand.answer }}</p>
|
||||
<span class="answer-time">回答时间: {{ demand.answered_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="status waiting">待回答</span>
|
||||
{% endif %}
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('edit_demand', id=demand.id) }}" class="btn btn-edit">编辑</a>
|
||||
<a href="{{ url_for('answer_demand', id=demand.id) }}" class="btn btn-answer">回答</a>
|
||||
<form method="POST" action="{{ url_for('toggle_public', id=demand.id) }}" style="display: inline;">
|
||||
<button type="submit" class="btn btn-toggle">
|
||||
{% if demand.is_public %}设为私有{% else %}设为公开{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty">暂无需求</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
24
templates/answer_form.html
Normal file
24
templates/answer_form.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}回答需求{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>回答需求</h2>
|
||||
<div class="question-preview">
|
||||
<h3>需求内容</h3>
|
||||
<h4 class="demand-title">{{ demand.title }}</h4>
|
||||
<p>{{ demand.content }}</p>
|
||||
<p class="meta">分会: {{ get_branch_name(demand.branch) }} | 联系方式: {{ demand.contact }}</p>
|
||||
</div>
|
||||
<form method="POST" class="form">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group">
|
||||
{{ form.answer.label }}
|
||||
{{ form.answer(class="form-control", rows=8) }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('index') }}" class="btn btn-cancel">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
47
templates/base.html
Normal file
47
templates/base.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}人事共享服务中心"码"上办{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1>
|
||||
<img src="{{ url_for('static', filename='logo.png') }}" alt="logo" class="header-logo">
|
||||
<a href="{{ url_for('index') }}">人事共享服务中心"码"上办</a>
|
||||
</h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('index') }}">需求汇总</a></li>
|
||||
{% if current_user.is_authenticated %}
|
||||
<li><a href="{{ url_for('new_demand') }}">提交新需求</a></li>
|
||||
<li><a href="{{ url_for('my_demands') }}">我的需求</a></li>
|
||||
{% if current_user.is_admin() %}
|
||||
<li><a href="{{ url_for('admin_demands') }}">管理后台</a></li>
|
||||
{% endif %}
|
||||
<li class="user-name">{{ current_user.dingtalk_name or current_user.username }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2026 人事共享服务中心"码"上办</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
31
templates/demand_form.html
Normal file
31
templates/demand_form.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ title }}</h2>
|
||||
<form method="POST" class="form">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group">
|
||||
<label>{{ form.title.label.text }} *</label>
|
||||
{{ form.title(class="form-control") }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ form.content.label.text }} *</label>
|
||||
{{ form.content(class="form-control textarea") }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ form.branch.label.text }}</label>
|
||||
{{ form.branch(class="form-control") }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ form.contact.label.text }} *</label>
|
||||
{{ form.contact(class="form-control") }}
|
||||
</div>
|
||||
{{ form.is_public() }} <label for="is_public">{{ form.is_public.label.text }}</label>
|
||||
<div class="form-group">
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('index') }}" class="btn btn-cancel">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
186
templates/dingtalk_entry.html
Normal file
186
templates/dingtalk_entry.html
Normal file
@@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>人事共享服务中心"码"上办</title>
|
||||
<script src="https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.15/dingtalk.open.js"></script>
|
||||
<!-- Aplus SDK for performance monitoring -->
|
||||
<script src="https://g.alicdn.com/a-plus/a-plus-web-sdk/1.1.2/index.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
.loading {
|
||||
font-size: 18px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.error {
|
||||
font-size: 16px;
|
||||
color: #ffcccc;
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: rgba(255,0,0,0.2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(255,255,255,0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="/static/logo.png" alt="logo" class="logo">
|
||||
<div class="spinner"></div>
|
||||
<p class="loading">正在跳转...</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('[前端] ============== 钉钉登录流程开始 ==============');
|
||||
const APP_KEY = '{{ app_key }}';
|
||||
const TARGET_URL = '{{ target_url }}';
|
||||
const CORP_ID = '{{ corp_id }}';
|
||||
const AGENT_ID = '{{ agent_id }}';
|
||||
console.log('[前端] 配置信息:', { APP_KEY, TARGET_URL, CORP_ID, AGENT_ID });
|
||||
|
||||
function showError(msg) {
|
||||
console.error('[前端] 错误:', msg);
|
||||
document.querySelector('.loading').textContent = '配置失败';
|
||||
document.querySelector('.spinner').style.display = 'none';
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'error';
|
||||
errorDiv.textContent = msg;
|
||||
document.querySelector('.container').appendChild(errorDiv);
|
||||
}
|
||||
|
||||
function getCleanUrl() {
|
||||
var url = window.location.protocol + '//' + window.location.hostname;
|
||||
if (window.location.port && window.location.port !== '80' && window.location.port !== '443') {
|
||||
url += ':' + window.location.port;
|
||||
}
|
||||
url += window.location.pathname;
|
||||
return url;
|
||||
}
|
||||
|
||||
function handleAuthCode(code) {
|
||||
console.log('[前端] 3. 获取到 code:', code);
|
||||
console.log('[前端] 4. 开始请求后端 /requirement-collection/dingtalk/api/getDingUser?code=' + code);
|
||||
|
||||
fetch('/requirement-collection/dingtalk/api/getDingUser?code=' + code)
|
||||
.then(function(resp) {
|
||||
console.log('[前端] 5. 收到后端响应, 状态码:', resp.status);
|
||||
return resp.json();
|
||||
})
|
||||
.then(function(user) {
|
||||
console.log('[前端] 6. 解析响应数据:', user);
|
||||
|
||||
if (user.userid) {
|
||||
var userId = user.userid;
|
||||
var name = user.name || user.nickname || '';
|
||||
var dept = user.department || '';
|
||||
if (Array.isArray(dept)) {
|
||||
dept = dept.join(',');
|
||||
}
|
||||
|
||||
console.log('[前端] 7. 用户信息解析:', { userId: userId, name: name, dept: dept });
|
||||
var targetUrl = TARGET_URL + '?userId=' + encodeURIComponent(userId) + '&name=' + encodeURIComponent(name) + '&dept=' + encodeURIComponent(dept);
|
||||
console.log('[前端] 8. 准备跳转:', targetUrl);
|
||||
window.location.href = targetUrl;
|
||||
} else {
|
||||
console.error('[前端] 7. 无userid, 无法登录');
|
||||
showError('获取用户信息失败,请重试');
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('[前端] 6. fetch请求失败:', err);
|
||||
showError('网络请求失败,请重试');
|
||||
});
|
||||
}
|
||||
|
||||
function initDingTalk() {
|
||||
var url = getCleanUrl();
|
||||
console.log('[前端] 0. 当前页面URL (清理后):', url);
|
||||
|
||||
var apiUrl = '/requirement-collection/dingtalk/api/getSignature?url=' + encodeURIComponent(url);
|
||||
console.log('[前端] 1. 请求后端获取签名:', apiUrl);
|
||||
|
||||
fetch(apiUrl)
|
||||
.then(function(resp) {
|
||||
console.log('[前端] 2. 收到响应, 状态码:', resp.status);
|
||||
return resp.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
console.log('[前端] 2. 收到签名数据:', data);
|
||||
|
||||
if (!data.signature) {
|
||||
showError('获取签名失败: ' + (data.error || '未知错误'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[前端] 3. 配置dd.config...');
|
||||
dd.config({
|
||||
agentId: data.agentId ? Number(data.agentId) : (AGENT_ID ? Number(AGENT_ID) : 0),
|
||||
corpId: CORP_ID,
|
||||
timeStamp: data.timeStamp,
|
||||
nonceStr: data.nonceStr,
|
||||
signature: data.signature,
|
||||
type: 0,
|
||||
jsApiList: ['runtime.permission.requestAuthCode']
|
||||
});
|
||||
|
||||
dd.ready(function() {
|
||||
console.log('[前端] 4. dd.ready 成功 - 钉钉SDK已就绪');
|
||||
console.log('[前端] 5. 开始调用 requestAuthCode 获取授权码...');
|
||||
|
||||
dd.runtime.permission.requestAuthCode({
|
||||
corpId: CORP_ID,
|
||||
onSuccess: function(res) {
|
||||
handleAuthCode(res.code);
|
||||
},
|
||||
onFail: function(err) {
|
||||
console.error('[前端] 6. requestAuthCode 失败:', err);
|
||||
showError('获取授权码失败: ' + JSON.stringify(err));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
dd.error(function(err) {
|
||||
console.error('[前端] 4. dd.error - 钉钉SDK配置失败:', err);
|
||||
showError('钉钉SDK配置失败: ' + JSON.stringify(err));
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error('[前端] 1. 请求签名失败:', err);
|
||||
showError('初始化失败: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
initDingTalk();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
51
templates/index.html
Normal file
51
templates/index.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}需求汇总{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>需求汇总</h2>
|
||||
{% if demands %}
|
||||
<div class="question-list">
|
||||
{% for demand in demands %}
|
||||
<div class="question-item">
|
||||
<h3 class="demand-title">{{ demand.title }} <span class="branch">{{ get_branch_name(demand.branch) }}</span></h3>
|
||||
<span class="time">{{ demand.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
<p class="content">{{ demand.content }}</p>
|
||||
{% if demand.answer %}
|
||||
<div class="answer-toggle" onclick="toggleAnswer(this)">
|
||||
<span class="toggle-icon">▶</span>
|
||||
<span class="toggle-text">查看回答</span>
|
||||
</div>
|
||||
<div class="answer-content" style="display: none;">
|
||||
<div class="answer">
|
||||
<strong>回答:</strong>
|
||||
<p>{{ demand.answer }}</p>
|
||||
<span class="answer-time">回答时间: {{ demand.answered_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty">暂无公开需求</p>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function toggleAnswer(element) {
|
||||
var content = element.nextElementSibling;
|
||||
var icon = element.querySelector('.toggle-icon');
|
||||
var text = element.querySelector('.toggle-text');
|
||||
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.textContent = '▼';
|
||||
text.textContent = '收起回答';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.textContent = '▶';
|
||||
text.textContent = '查看回答';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
56
templates/my_demands.html
Normal file
56
templates/my_demands.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}我的需求{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>我的需求</h2>
|
||||
{% if demands %}
|
||||
<div class="question-list">
|
||||
{% for demand in demands %}
|
||||
<div class="question-item">
|
||||
<h3 class="demand-title">{{ demand.title }} <span class="branch">{{ get_branch_name(demand.branch) }}</span></h3>
|
||||
<span class="time">{{ demand.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
<p class="content">{{ demand.content }}</p>
|
||||
{% if demand.answer %}
|
||||
<div class="answer-toggle" onclick="toggleAnswer(this)">
|
||||
<span class="toggle-icon">▶</span>
|
||||
<span class="toggle-text">查看回答</span>
|
||||
</div>
|
||||
<div class="answer-content" style="display: none;">
|
||||
<div class="answer">
|
||||
<strong>回答:</strong>
|
||||
<p>{{ demand.answer }}</p>
|
||||
<span class="answer-time">回答时间: {{ demand.answered_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not demand.answer %}
|
||||
<div class="actions">
|
||||
<a href="{{ url_for('edit_demand', id=demand.id) }}" class="btn btn-edit">编辑</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty">暂无需求</p>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function toggleAnswer(element) {
|
||||
var content = element.nextElementSibling;
|
||||
var icon = element.querySelector('.toggle-icon');
|
||||
var text = element.querySelector('.toggle-text');
|
||||
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.textContent = '▼';
|
||||
text.textContent = '收起回答';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.textContent = '▶';
|
||||
text.textContent = '查看回答';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
11
templates/not_logged_in.html
Normal file
11
templates/not_logged_in.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}未登录 - 人事共享服务中心"码"上办{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="login-required">
|
||||
<div class="login-icon">🔒</div>
|
||||
<h2>请先通过钉钉登录</h2>
|
||||
<p>本系统需要通过钉钉账号登录才能使用</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user