diff --git a/.flaskenv b/.flaskenv new file mode 100644 index 0000000..49ff866 --- /dev/null +++ b/.flaskenv @@ -0,0 +1,6 @@ +FLASK_APP=inventory_check_lark +FLASK_ENV=development +SECRET_KEY=asdASS3434dfE5tgfg +LARK_APP_ID=cli_a7359bac85b9d01c +LARK_APP_SECRECT=vZdiNrSiszH9ODD2qzuI1fHRupOwY5wY +LARK_TB_TOKEN=EciWbKOEXa68HIsnh5Ac7vZgnff \ No newline at end of file diff --git a/README.md b/README.md index 6bb9df9..3616fd0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ -# inventory_check_lark +# inventory_check + +Flask +gunicorn +python-dotenv +lark-oapi +Flask-SQLAlchemy \ No newline at end of file diff --git a/data.db b/data.db new file mode 100644 index 0000000..c240dc9 Binary files /dev/null and b/data.db differ diff --git a/inventory_check_lark/__init__.py b/inventory_check_lark/__init__.py new file mode 100644 index 0000000..054dd19 --- /dev/null +++ b/inventory_check_lark/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" + :author: luffmims + :url: http://yum2.cc + :copyright: © 2025 luffmims + :license: MIT, see LICENSE for more details. +""" +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +application = Flask('inventory_check_lark') +application.config.from_pyfile('settings.py') +application.jinja_env.trim_blocks = True +application.jinja_env.lstrip_blocks = True + +db = SQLAlchemy(application) + +from inventory_check_lark import views, errors, commands diff --git a/inventory_check_lark/commands.py b/inventory_check_lark/commands.py new file mode 100644 index 0000000..564892f --- /dev/null +++ b/inventory_check_lark/commands.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" + :author: luffmims + :url: http://yum2.cc + :copyright: © 2025 luffmims + :license: MIT, see LICENSE for more details. +""" +import click + +from inventory_check_lark import application + + + +@application.cli.command() +@click.option('--drop', is_flag=True, help='Create after drop.') +def initdb(drop): + """Initialize the database.""" + click.echo('Initialized database.') + diff --git a/inventory_check_lark/errors.py b/inventory_check_lark/errors.py new file mode 100644 index 0000000..64b00fe --- /dev/null +++ b/inventory_check_lark/errors.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +""" + :author: luffmims + :url: http://yum2.cc + :copyright: © 2025 luffmims + :license: MIT, see LICENSE for more details. +""" +from flask import render_template + +from inventory_check_lark import application + + +@application.errorhandler(404) +def page_not_found(e): + return render_template('errors/404.html'), 404 + + +@application.errorhandler(500) +def internal_server_error(e): + return render_template('errors/500.html'), 500 diff --git a/inventory_check_lark/models.py b/inventory_check_lark/models.py new file mode 100644 index 0000000..f598511 --- /dev/null +++ b/inventory_check_lark/models.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +""" + :author: luffmims + :url: http://yum2.cc + :copyright: © 2025 luffmims + :license: MIT, see LICENSE for more details. +""" + +# from datetime import datetime +from inventory_check_lark import application, db +import json +import time +import lark_oapi as lark +from lark_oapi.api.auth.v3 import * +from lark_oapi.api.bitable.v1 import * + +class Inventory(db.Model): + id = db.Column(db.Integer, primary_key=True) + tag = db.Column(db.Text) + amount = db.Column(db.Integer) + checked = db.Column(db.Boolean, default=False) + recordid = db.Column(db.Text) + tableid = db.Column(db.Text) + # lasttime = db.Column(db.DateTime, default=datetime.now()) + +client = lark.Client.builder() \ + .app_id(application.config['LARK_APP_ID']) \ + .app_secret(application.config['LARK_APP_SECRECT']) \ + .log_level(lark.LogLevel.DEBUG) \ + .build() + +TIMER = time.time() +def get_access_token(): + # 构造请求对象 + request: InternalAppAccessTokenRequest = InternalAppAccessTokenRequest.builder() \ + .request_body(InternalAppAccessTokenRequestBody.builder() + .app_id(application.config['LARK_APP_ID']) + .app_secret(application.config['LARK_APP_SECRECT']) + .build()) \ + .build() + + # 发起请求 + response: InternalAppAccessTokenResponse = client.auth.v3.app_access_token.internal(request) + + # 处理失败返回 + # if not response.success(): + # lark.logger.error( + # f"client.auth.v3.app_access_token.internal failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + # return + if not response.success(): + return False + return True + # # 处理业务结果 + # res = eval(response.raw.content) + # return {"access_token": res["tenant_access_token"], "expire": res["expire"], "code": res["code"]} + +def get_table_record(id, page_token=None): + global TIMER + if time.time() - TIMER > 7000: + get_access_token() + TIMER = time.time() + # 构造请求对象 + if page_token: + request: SearchAppTableRecordRequest = SearchAppTableRecordRequest.builder() \ + .app_token(application.config['LARK_TB_TOKEN']) \ + .table_id(id['table_id']) \ + .page_token(page_token) \ + .page_size(50) \ + .request_body(SearchAppTableRecordRequestBody.builder() + .view_id(id['view_id']) + .field_names(["规格", "外库数量", "排序", "已盘"]) + .sort([Sort.builder() + .field_name("排序") + .desc(False) + .build() + ]) + .automatic_fields(True) + .build()) \ + .build() + else: + request: SearchAppTableRecordRequest = SearchAppTableRecordRequest.builder() \ + .app_token(application.config['LARK_TB_TOKEN']) \ + .table_id(id['table_id']) \ + .page_size(50) \ + .request_body(SearchAppTableRecordRequestBody.builder() + .view_id(id['view_id']) + .field_names(["规格", "外库数量", "排序", "已盘"]) + .sort([Sort.builder() + .field_name("排序") + .desc(False) + .build() + ]) + .automatic_fields(True) + .build()) \ + .build() + + # 发起请求 + response: SearchAppTableRecordResponse = client.bitable.v1.app_table_record.search(request) + + # 处理失败返回 + if not response.success(): + lark.logger.error( + f"client.bitable.v1.app_table_record.search failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + return + rep = json.loads(response.raw.content) + res = [] + page_token = None + if rep['data']['has_more']: + page_token = rep['data']['page_token'] + for i in rep['data']['items']: + if '已盘' in i['fields']: + cd = i['fields']['已盘'] + else: + cd = False + res.append({ + "sort": int(i['fields']['排序']), + "tag": i['fields']['规格'][0]['text'], + "amount": int(i['fields']['外库数量']) if '外库数量' in i['fields'] else 0, + "checked": cd, + "recordid": i['record_id'], + }) + + return res, page_token + +def update_record(id, recordid, amount): + global TIMER + if time.time() - TIMER > 200: + get_access_token() + TIMER = time.time() + # 构造请求对象 + request: UpdateAppTableRecordRequest = UpdateAppTableRecordRequest.builder() \ + .app_token(application.config['LARK_TB_TOKEN']) \ + .table_id(id['table_id']) \ + .record_id(recordid) \ + .request_body(AppTableRecord.builder() + .fields({"已盘":True,"外库数量":amount}) + .build()) \ + .build() + + # 发起请求 + response: UpdateAppTableRecordResponse = client.bitable.v1.app_table_record.update(request) + + # 处理失败返回 + # if not response.success(): + # lark.logger.error( + # f"client.bitable.v1.app_table_record.update failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}") + # return + if not response.success(): + return False + # 处理业务结果 + # lark.logger.info(lark.JSON.marshal(response.raw.content, indent=4)) + return True + +def init_db(): + get_access_token() + with application.app_context(): + db.drop_all() + db.create_all() + + youxin, page_token= get_table_record(application.config['YOUXIN_ID']) + for i in youxin: + inv = Inventory() + inv.id = i['sort'] + inv.tag = i['tag'] + inv.amount = i['amount'] + inv.checked = i['checked'] + inv.recordid = i['recordid'] + inv.tableid = application.config['YOUXIN_ID'] + db.session.add(inv) + while page_token: + youxin, page_token = get_table_record(application.config['YOUXIN_ID'], page_token) + if not youxin: + break + for i in youxin: + inv = Inventory() + inv.id = i['sort'] + inv.tag = i['tag'] + inv.amount = i['amount'] + inv.checked = i['checked'] + inv.recordid = i['recordid'] + inv.tableid = application.config['YOUXIN_ID'] + db.session.add(inv) + + waimo, page_token= get_table_record(application.config['WAIMO_ID']) + for i in waimo: + inv = Inventory() + inv.id = i['sort'] + inv.tag = i['tag'] + inv.amount = i['amount'] + inv.checked = i['checked'] + inv.recordid = i['recordid'] + inv.tableid = application.config['WAIMO_ID'] + db.session.add(inv) + while page_token: + waimo, page_token = get_table_record(application.config['WAIMO_ID'], page_token) + if not waimo: + break + for i in waimo: + inv = Inventory() + inv.id = i['sort'] + inv.tag = i['tag'] + inv.amount = i['amount'] + inv.checked = i['checked'] + inv.recordid = i['recordid'] + inv.tableid = application.config['WAIMO_ID'] + db.session.add(inv) + + + db.session.commit() + return True + diff --git a/inventory_check_lark/settings.py b/inventory_check_lark/settings.py new file mode 100644 index 0000000..ceb4760 --- /dev/null +++ b/inventory_check_lark/settings.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +""" + :author: luffmims + :url: http://yum2.cc + :copyright: © 2025 luffmims + :license: MIT, see LICENSE for more details. +""" +import os +import sys + +from inventory_check_lark import application + +# sqlite URI compatible +WIN = sys.platform.startswith('win') +if WIN: + prefix = 'sqlite:///' +else: + prefix = 'sqlite:////' + +dev_db = prefix + os.path.join(os.path.dirname(application.root_path), 'data.db') + +SQLALCHEMY_TRACK_MODIFICATIONS = False +SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URI', dev_db) +SECRET_KEY = os.getenv('SECRET_KEY', 'secret string') + +application.config['LARK_APP_ID'] = os.getenv('LARK_APP_ID', 'lark_app_id') +application.config['LARK_APP_SECRECT'] = os.getenv('LARK_APP_SECRECT', 'lark_app_secrect') +application.config['LARK_TB_TOKEN'] = os.getenv('LARK_TB_TOKEN', 'lark_app_secrect') + +application.config['WAIMO_ID'] = { + 'table_id': 'tbluSufkcQm0noBz', + 'view_id': 'vewLYvBFns', + 'tag_id': 'fld2xdCqrs', + 'amount_id': 'fldC9cnnAY', + 'checked_id': 'fldJkHhQgQ' +} + +application.config['YOUXIN_ID'] = { + 'table_id': 'tbl9NPZQMl5IVHQ2', + 'view_id': 'vewrOuK32q', + 'tag_id': 'fldrxqL8uz', + 'amount_id': 'fldsOIpOBk', + 'checked_id': 'fldsuS6SuR' +} + diff --git a/inventory_check_lark/static/css/style.css b/inventory_check_lark/static/css/style.css new file mode 100644 index 0000000..01ec2d7 --- /dev/null +++ b/inventory_check_lark/static/css/style.css @@ -0,0 +1,39 @@ +body { + background-color: azure; +} +main { + text-align: center; + vertical-align: middle; + font-size: 18px; +} +input { + width: 4ch; + text-align: center; +} +hr { + width: 350px; + height: 3px; +} +.grail{ + display: flex; + min-height: 100vh; + flex-direction: column; + flex-wrap: nowrap; + justify-content: space-evenly; + align-items: center; +} +.handler { + width: 350px; + display: flex; + flex-wrap: wrap; + align-content: space-between; + flex-direction: row; + justify-content: space-between; + align-items: center; +} +button, input { + font-size: 18px; +} +form { + width:350px; +} \ No newline at end of file diff --git a/inventory_check_lark/static/favicon.ico b/inventory_check_lark/static/favicon.ico new file mode 100644 index 0000000..7962630 Binary files /dev/null and b/inventory_check_lark/static/favicon.ico differ diff --git a/inventory_check_lark/static/js/script.js b/inventory_check_lark/static/js/script.js new file mode 100644 index 0000000..c9de89f --- /dev/null +++ b/inventory_check_lark/static/js/script.js @@ -0,0 +1,51 @@ +function increment() { + var amount = document.getElementById("inventory-amount"); + amount.value = parseInt(amount.value) + 1; +} +function decrement() { + var amount = document.getElementById("inventory-amount"); + if (amount.value > 0) { + amount.value = parseInt(amount.value) - 1; + } +} +function checknext() { + var amount = document.getElementById("inventory-amount"); + // 获取ID为"forid"的元素 + var idelement = document.getElementById("inventory-id"); + // 获取该元素的文本内容,并解析为整数 + var id = parseInt(idelement.innerText, 10); // 第二个参数10表示解析的基数,这里是十进制 + var tagelement = document.getElementById("inventory-tag"); + var tag = tagelement.innerText; + var msg = document.getElementById("flash-msg"); + var location = document.getElementById("inventory-location"); + // var params = new URLSearchParams({ + // checked: 1, + // id: id, + // amount: amount.value + // }); + // var url = `/check?${params.toString()}`; + fetch('/get_inventory', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({id: id, amount: amount.value}) + }) + .then(response => response.json()) + .then(data => { + // console.log('Success:', data); + amount.value = data.amount; + tagelement.innerText = data.tag; + idelement.innerText = data.id; + msg.innerText = data.checkedone + " 盘点数 " + data.checkedamount; + location.innerText = data.location; + + }) + .catch(error => console.error('Error:', error)); + +} + +function backtoroot(msg="已完成,返回") { + alert(msg); + location.href="/"; +} \ No newline at end of file diff --git a/inventory_check_lark/templates/after.html b/inventory_check_lark/templates/after.html new file mode 100644 index 0000000..2354f66 --- /dev/null +++ b/inventory_check_lark/templates/after.html @@ -0,0 +1,16 @@ + + + + + + 盘库助手 + + + + + + + + \ No newline at end of file diff --git a/inventory_check_lark/templates/base.html b/inventory_check_lark/templates/base.html new file mode 100644 index 0000000..d59f562 --- /dev/null +++ b/inventory_check_lark/templates/base.html @@ -0,0 +1,28 @@ + + + + + + 盘库助手 + + + + +
+
+

盘点

+
+ + {% block content %}{% endblock %} +
+ {% block footer %} + 2025 © SDHLMH + + {% endblock %} +
+
+ + +{% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/inventory_check_lark/templates/check.html b/inventory_check_lark/templates/check.html new file mode 100644 index 0000000..4d2adfc --- /dev/null +++ b/inventory_check_lark/templates/check.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block content %} +
+ 开始盘点>>> +
+
+
+ +
+
+

{{ inventory.tag }}

+
+
+ + + +
+
+
+
+
+ +
+
+{% endblock %} + diff --git a/inventory_check_lark/templates/errors/404.html b/inventory_check_lark/templates/errors/404.html new file mode 100644 index 0000000..6dcb741 --- /dev/null +++ b/inventory_check_lark/templates/errors/404.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block title %}404 Error{% endblock %} + +{% block content %} +

Page Not Found

+{% endblock %} + +{% block footer %} + ← Go Back +{% endblock %} \ No newline at end of file diff --git a/inventory_check_lark/templates/errors/500.html b/inventory_check_lark/templates/errors/500.html new file mode 100644 index 0000000..e1f2c95 --- /dev/null +++ b/inventory_check_lark/templates/errors/500.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block title %}500 Error{% endblock %} + +{% block content %} +

Internal Server Error

+{% endblock %} + +{% block footer %} + ← Go Back +{% endblock %} \ No newline at end of file diff --git a/inventory_check_lark/templates/index.html b/inventory_check_lark/templates/index.html new file mode 100644 index 0000000..2a1840d --- /dev/null +++ b/inventory_check_lark/templates/index.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block content %} + +
+ +
+
+ +
+ +
+
+{% endblock %} diff --git a/inventory_check_lark/templates/init.html b/inventory_check_lark/templates/init.html new file mode 100644 index 0000000..eb956e9 --- /dev/null +++ b/inventory_check_lark/templates/init.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block title %}上传Excel文件{% endblock %} + +{% block content %} +

初始化

+
+ +
+
+
+
+
初始化是从多维表格获取数据,修改多维表格后务必运行,花费约1-5分钟,请耐心等待
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/inventory_check_lark/views.py b/inventory_check_lark/views.py new file mode 100644 index 0000000..b79a210 --- /dev/null +++ b/inventory_check_lark/views.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" + :author: luffmims + :url: http://yum2.cc + :copyright: © 2025 luffmims + :license: MIT, see LICENSE for more details. +""" + +from flask import jsonify, request, render_template +from inventory_check_lark import application, db +from inventory_check_lark.models import init_db, get_access_token, get_table_record, update_record, Inventory +from werkzeug.exceptions import BadRequest + +@application.route('/') +def index(): + return render_template('index.html') + +@application.route('/init') +def init(): + return 'ok' if init_db() else 'error' + +@application.route('/check', methods=['GET']) +def check(): + inventory = Inventory.query.filter_by(checked=False).first() + if not inventory: + return render_template('after.html',msg="未初始化或已盘完") + return render_template('check.html',inventory=inventory) + +@application.route('/get_youxin_inventory', methods=['POST']) +def update_youxin_inventory(): + try: + # 获取并验证表单数据 + data = request.get_json() + inventory_id = data['id'] + amount = data['amount'] + + if not inventory_id or not amount: + raise BadRequest('Missing inventory id or amount') + + # 查询并更新库存对象 + inventory = Inventory.query.get(inventory_id) + if not inventory: + raise BadRequest('Inventory not found') + checkedone = inventory.tag + inventory.amount = amount + inventory.checked = True + update_record(inventory.tableid, inventory.recordid, amount) + + # 提交更改 + db.session.commit() + + # 显示成功消息 + # flash(f'{inventory.tag} checked and updated') + id = inventory_id + 1 + inventory = Inventory.query.get(id) + if not inventory: + raise BadRequest('Inventory not found') + # 返回更新后的库存信息 + return jsonify({ + 'id': inventory.id, + 'tag': inventory.tag, + 'amount': inventory.amount, + 'checkedone': checkedone, + 'checkedamount': amount + }) + + except BadRequest as e: + # 处理错误情况 + # flash(str(e)) + return jsonify({'error': str(e)}), 400 + + except Exception as e: + # 处理其他可能的错误 + db.session.rollback() + # flash('An error occurred while updating the inventory') + return jsonify({'error': 'Internal server error'}), 500 \ No newline at end of file diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..2cb8c48 --- /dev/null +++ b/wsgi.py @@ -0,0 +1 @@ +from inventory_check_lark import application