初始提交:人事共享服务中心钉钉登录功能
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