From 656fe2fc51409f1abbeb3f1423afb0edd44c14d4 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 18 Jun 2026 13:33:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BB=A3=E5=AE=A2?= =?UTF-8?q?=E5=85=85=E5=80=BC=E5=92=8C=E9=94=99=E5=B8=90=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - proxy_recharge.ui: Form表单输入客户用户名+充值金额 - proxy_recharge_submit.dspy: 查找用户+创建payment_log+调用recharge_accounting - error_accounting.ui: 错帐处理管理页面(placeholder,含设计文档) --- wwwroot/error_accounting.ui | 497 ++++++++++++++++++ .../open_customer_accounts_with_orgid.dspy | 8 + wwwroot/proxy_recharge.ui | 82 +++ wwwroot/proxy_recharge_submit.dspy | 148 ++++++ 4 files changed, 735 insertions(+) create mode 100644 wwwroot/error_accounting.ui create mode 100644 wwwroot/open_customer_accounts_with_orgid.dspy create mode 100644 wwwroot/proxy_recharge.ui create mode 100644 wwwroot/proxy_recharge_submit.dspy diff --git a/wwwroot/error_accounting.ui b/wwwroot/error_accounting.ui new file mode 100644 index 0000000..f2becc5 --- /dev/null +++ b/wwwroot/error_accounting.ui @@ -0,0 +1,497 @@ +{# + 错帐处理 (Error Accounting) Page + + PURPOSE: + This page provides a management interface for handling accounting errors + and exceptions. It allows operators to review, correct, and resolve + accounting discrepancies. + + INTENDED WORKFLOW: + 1. An accounting exception is detected (manually or automatically): + - wrong_account: Transaction posted to incorrect account/subject + - duplicate_entry: Same transaction recorded more than once + - missing_entry: Expected transaction not found in records + - amount_mismatch: Debit/credit amounts don't balance or differ from source + + 2. Operator reviews the error log table (error_accounting_log): + - Each row shows: timestamp, error type, original transaction info, status + - Filter by status (pending/resolved) to prioritize work + + 3. Operator selects an error record and chooses a correction action: + - reverse_entry: Create a reversing journal entry to cancel the original + - adjust_entry: Create an adjustment entry to correct the amount/account + - mark_resolved: Flag as resolved without further action (e.g., duplicate already fixed) + + 4. The correction is recorded with an audit trail linking back to the original error. + + DATA SOURCE: + - Table: error_accounting_log + - Expected fields: id, timestamp, error_type, original_trans_id, + original_subject, original_amount, original_summary, + error_description, status, resolved_at, resolved_by, correction_action + + TOOLBAR ACTIONS: + - 报告错帐: Opens a form to manually report a new accounting error + - 全部: Show all error records (no filter) + - 待处理: Filter to show only pending/unresolved errors + - 已处理: Filter to show only resolved errors + + ROW ACTIONS (on click): + - 冲正 (reverse_entry): Reverse the original transaction + - 调整 (adjust_entry): Create an adjustment/correction entry + - 标记已处理 (mark_resolved): Mark as resolved +#} +{ + "widgettype": "VBox", + "id": "error_accounting_page", + "options": { + "width": "100%", + "height": "100%", + "gap": "12px", + "padding": "16px" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "id": "error_acc_header", + "options": { + "bgcolor": "#1E293B", + "padding": "20px", + "borderRadius": "12px", + "border": "1px solid #334155", + "gap": "8px" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "alignItems": "center", + "justifyContent": "space-between" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": {"gap": "4px"}, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "错帐处理", + "fontSize": "22px", + "fontWeight": "700", + "color": "#F1F5F9" + } + }, + { + "widgettype": "Text", + "options": { + "text": "会计差错管理 — 发现、纠正并解决帐务异常记录", + "fontSize": "13px", + "color": "#94A3B8" + } + } + ] + } + ] + } + ] + }, + { + "widgettype": "HBox", + "id": "error_acc_toolbar", + "options": { + "bgcolor": "#1E293B", + "padding": "12px 16px", + "borderRadius": "10px", + "border": "1px solid #334155", + "alignItems": "center", + "gap": "10px" + }, + "subwidgets": [ + { + "widgettype": "Button", + "id": "btn_report_error", + "options": { + "label": "报告错帐", + "bgcolor": "#EF4444", + "color": "#FFFFFF", + "borderRadius": "6px", + "padding": "8px 16px", + "fontWeight": "600" + }, + "binds": [{ + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": { + "title": "报告错帐", + "width": "560px", + "height": "600px" + }, + "options": { + "url": "{{entire_url('/accounting/error_accounting_report.ui')}}" + } + }] + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "Text", + "options": { + "text": "筛选:", + "fontSize": "13px", + "color": "#94A3B8" + } + }, + { + "widgettype": "Button", + "id": "btn_filter_all", + "options": { + "label": "全部", + "bgcolor": "#3B82F6", + "color": "#FFFFFF", + "borderRadius": "6px", + "padding": "6px 12px" + }, + "binds": [{ + "wid": "self", + "event": "click", + "actiontype": "script", + "script": "const tab = bricks.getWidgetById('error_acc_tabular'); if(tab) { tab.render({}); }" + }] + }, + { + "widgettype": "Button", + "id": "btn_filter_pending", + "options": { + "label": "待处理", + "bgcolor": "#F59E0B", + "color": "#FFFFFF", + "borderRadius": "6px", + "padding": "6px 12px" + }, + "binds": [{ + "wid": "self", + "event": "click", + "actiontype": "script", + "script": "const tab = bricks.getWidgetById('error_acc_tabular'); if(tab) { tab.render({status: 'pending'}); }" + }] + }, + { + "widgettype": "Button", + "id": "btn_filter_resolved", + "options": { + "label": "已处理", + "bgcolor": "#22C55E", + "color": "#FFFFFF", + "borderRadius": "6px", + "padding": "6px 12px" + }, + "binds": [{ + "wid": "self", + "event": "click", + "actiontype": "script", + "script": "const tab = bricks.getWidgetById('error_acc_tabular'); if(tab) { tab.render({status: 'resolved'}); }" + }] + } + ] + }, + { + "widgettype": "VBox", + "id": "error_acc_table_container", + "options": { + "bgcolor": "#1E293B", + "borderRadius": "10px", + "border": "1px solid #334155", + "width": "100%", + "flex": "1" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "bgcolor": "#0F172A", + "padding": "10px 16px", + "borderRadius": "10px 10px 0 0", + "alignItems": "center" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": {"text": "时间", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 16} + }, + { + "widgettype": "Text", + "options": {"text": "错帐类型", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 12} + }, + { + "widgettype": "Text", + "options": {"text": "原始交易", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 20} + }, + { + "widgettype": "Text", + "options": {"text": "金额", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 10} + }, + { + "widgettype": "Text", + "options": {"text": "说明", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 24} + }, + { + "widgettype": "Text", + "options": {"text": "状态", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 8} + }, + { + "widgettype": "Text", + "options": {"text": "操作", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 10} + } + ] + }, + { + "widgettype": "Tabular", + "id": "error_acc_tabular", + "options": { + "width": "100%", + "height": "100%", + "css": "filler", + "data_url": "{{entire_url('/accounting/error_accounting_log.dspy')}}", + "editable": false, + "page_rows": 50, + "row_options": { + "browserfields": { + "exclouded": ["row_num_"] + }, + "fields": [ + { + "name": "timestamp", + "title": "时间", + "type": "timestamp", + "uitype": "timestamp", + "datatype": "timestamp", + "label": "时间", + "cwidth": 16 + }, + { + "name": "error_type", + "title": "错帐类型", + "type": "str", + "length": 20, + "uitype": "str", + "datatype": "str", + "label": "错帐类型", + "cwidth": 12 + }, + { + "name": "original_summary", + "title": "原始交易", + "type": "str", + "length": 100, + "uitype": "str", + "datatype": "str", + "label": "原始交易", + "cwidth": 20 + }, + { + "name": "original_amount", + "title": "金额", + "type": "float", + "length": 18, + "dec": 4, + "uitype": "float", + "datatype": "float", + "label": "金额", + "cwidth": 10 + }, + { + "name": "error_description", + "title": "说明", + "type": "str", + "length": 200, + "uitype": "str", + "datatype": "str", + "label": "说明", + "cwidth": 24 + }, + { + "name": "status", + "title": "状态", + "type": "str", + "length": 10, + "uitype": "str", + "datatype": "str", + "label": "状态", + "cwidth": 8 + } + ] + } + } + }, + { + "widgettype": "VBox", + "id": "error_acc_empty_hint", + "options": { + "padding": "20px", + "alignItems": "center", + "bgcolor": "#1E293B" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "如表格为空,表示暂无错帐记录。请点击「报告错帐」按钮手动添加,或确认 error_accounting_log 数据源已配置。", + "fontSize": "13px", + "color": "#64748B" + } + } + ] + } + ] + }, + { + "widgettype": "VBox", + "id": "error_acc_legend", + "options": { + "bgcolor": "#1E293B", + "padding": "16px", + "borderRadius": "10px", + "border": "1px solid #334155", + "gap": "8px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "错帐类型说明", + "fontSize": "14px", + "fontWeight": "600", + "color": "#F1F5F9" + } + }, + { + "widgettype": "HBox", + "options": { + "gap": "16px", + "alignItems": "center" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"gap": "6px", "alignItems": "center"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "bgcolor": "#EF444433", + "padding": "2px 8px", + "borderRadius": "4px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": {"text": "科目错误", "fontSize": "12px", "color": "#EF4444"} + } + ] + }, + { + "widgettype": "Text", + "options": {"text": "wrong_account", "fontSize": "11px", "color": "#64748B"} + } + ] + }, + { + "widgettype": "HBox", + "options": {"gap": "6px", "alignItems": "center"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "bgcolor": "#F59E0B33", + "padding": "2px 8px", + "borderRadius": "4px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": {"text": "重复入帐", "fontSize": "12px", "color": "#F59E0B"} + } + ] + }, + { + "widgettype": "Text", + "options": {"text": "duplicate_entry", "fontSize": "11px", "color": "#64748B"} + } + ] + }, + { + "widgettype": "HBox", + "options": {"gap": "6px", "alignItems": "center"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "bgcolor": "#8B5CF633", + "padding": "2px 8px", + "borderRadius": "4px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": {"text": "漏记", "fontSize": "12px", "color": "#8B5CF6"} + } + ] + }, + { + "widgettype": "Text", + "options": {"text": "missing_entry", "fontSize": "11px", "color": "#64748B"} + } + ] + }, + { + "widgettype": "HBox", + "options": {"gap": "6px", "alignItems": "center"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "bgcolor": "#3B82F633", + "padding": "2px 8px", + "borderRadius": "4px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": {"text": "金额不符", "fontSize": "12px", "color": "#3B82F6"} + } + ] + }, + { + "widgettype": "Text", + "options": {"text": "amount_mismatch", "fontSize": "11px", "color": "#64748B"} + } + ] + } + ] + }, + { + "widgettype": "HBox", + "options": { + "gap": "16px", + "alignItems": "center", + "marginTop": "4px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "text": "纠正操作: 冲正(reverse_entry) | 调整(adjust_entry) | 标记已处理(mark_resolved)", + "fontSize": "12px", + "color": "#94A3B8" + } + } + ] + } + ] + } + ] +} diff --git a/wwwroot/open_customer_accounts_with_orgid.dspy b/wwwroot/open_customer_accounts_with_orgid.dspy new file mode 100644 index 0000000..7075db2 --- /dev/null +++ b/wwwroot/open_customer_accounts_with_orgid.dspy @@ -0,0 +1,8 @@ +debug(f'{params_kw=}') +dbname = get_module_dbname('accounting') +orgid = await get_userorgid() +db = DBPools() +async with db.sqlorContext(dbname) as sor: + await openCustomerAccounts(sor, '0', orgid) + return f'{orgid} customer accounts opened' +return f'{db.e_except=}' diff --git a/wwwroot/proxy_recharge.ui b/wwwroot/proxy_recharge.ui new file mode 100644 index 0000000..bc89955 --- /dev/null +++ b/wwwroot/proxy_recharge.ui @@ -0,0 +1,82 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "padding": "16px", + "gap": "16px" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "bgcolor": "#1E293B", + "borderRadius": "10px", + "border": "1px solid #334155", + "padding": "20px", + "cwidth": 40 + }, + "subwidgets": [ + { + "widgettype": "Title3", + "options": {"text": "代客充值", "color": "#F1F5F9"} + }, + { + "widgettype": "Text", + "options": { + "text": "输入客户用户名和充值金额,由管理员代为客户完成充值操作。", + "color": "#94A3B8", + "fontSize": "13px" + } + }, + { + "widgettype": "Form", + "id": "proxy_recharge_form", + "options": { + "name": "proxy_recharge", + "submit_url": "{{entire_url('/accounting/proxy_recharge_submit.dspy')}}", + "show_label": true, + "submit_label": "确认充值", + "submit_css": "primary", + "fields": [ + { + "name": "username", + "label": "客户用户名", + "uitype": "str", + "required": true, + "placeholder": "输入客户用户名", + "cwidth": 20 + }, + { + "name": "amount", + "label": "充值金额", + "uitype": "float", + "required": true, + "placeholder": "输入充值金额", + "cwidth": 20 + } + ] + }, + "binds": [{ + "wid": "self", + "event": "submit", + "actiontype": "urldata", + "target": "recharge_result", + "options": { + "url": "{{entire_url('/accounting/proxy_recharge_submit.dspy')}}" + } + }] + }, + { + "widgettype": "VBox", + "id": "recharge_result", + "options": { + "width": "100%", + "padding": "8px" + }, + "subwidgets": [] + } + ] + } + ] +} diff --git a/wwwroot/proxy_recharge_submit.dspy b/wwwroot/proxy_recharge_submit.dspy new file mode 100644 index 0000000..ef5ce4a --- /dev/null +++ b/wwwroot/proxy_recharge_submit.dspy @@ -0,0 +1,148 @@ + +action = params_kw.get('action', 'submit') + +# ---- Lookup mode: find customer by username, return info ---- +if action == 'lookup': + username = params_kw.get('username', '').strip() + if not username: + return json.dumps({'status': 'error', 'message': '用户名不能为空'}, ensure_ascii=False, default=str) + + db = DBPools() + dbname = get_module_dbname('accounting') + async with db.sqlorContext(dbname) as sor: + sql = """ + select + u.username, + u.orgid as customerid, + o.orgname, + a.id as accountid, + a.balance + from users u + left join organization o on u.orgid = o.id COLLATE utf8mb4_unicode_ci + left join account a on a.orgid = u.orgid COLLATE utf8mb4_unicode_ci + where u.username = ${username}$ + limit 1 + """ + recs = await sor.sqlExe(sql, {'username': username}) + if not recs or len(recs) == 0: + return json.dumps({'status': 'error', 'message': f'用户 {username} 不存在'}, ensure_ascii=False, default=str) + + rec = recs[0] + return json.dumps({ + 'status': 'ok', + 'data': { + 'username': rec.username, + 'customerid': rec.customerid, + 'orgname': rec.orgname or '', + 'accountid': rec.accountid or '', + 'balance': float(rec.balance) if rec.balance else 0.0 + } + }, ensure_ascii=False, default=str) + + +# ---- Submit mode: process the proxy recharge ---- +username = params_kw.get('username', '').strip() +amount_raw = params_kw.get('amount', 0) + +if not username: + return { + "widgettype": "Text", + "options": {"text": "❌ 用户名不能为空", "color": "#EF4444"} + } + +try: + amount = float(amount_raw) +except (ValueError, TypeError): + return { + "widgettype": "Text", + "options": {"text": "❌ 充值金额格式错误", "color": "#EF4444"} + } + +if amount <= 0: + return { + "widgettype": "Text", + "options": {"text": "❌ 充值金额必须大于0", "color": "#EF4444"} + } + +userid = await get_user() +userorgid = await get_userorgid() +db = DBPools() + +# Look up the target customer by username +dbname = get_module_dbname('accounting') +async with db.sqlorContext(dbname) as sor: + sql = """ + select + u.username, + u.orgid as customerid, + o.orgname, + a.id as accountid + from users u + left join organization o on u.orgid = o.id COLLATE utf8mb4_unicode_ci + left join account a on a.orgid = u.orgid COLLATE utf8mb4_unicode_ci + where u.username = ${username}$ + limit 1 + """ + recs = await sor.sqlExe(sql, {'username': username}) + if not recs or len(recs) == 0: + return { + "widgettype": "Text", + "options": {"text": f"❌ 找不到用户名: {username}", "color": "#EF4444"} + } + + customer = recs[0] + customerid = customer.customerid + + if customerid == userorgid: + return { + "widgettype": "Text", + "options": {"text": "❌ 不能给自己进行代客充值", "color": "#EF4444"} + } + + # Create payment log in unipay for audit trail + unipay_dbname = get_module_dbname('unipay') + async with db.sqlorContext(unipay_dbname) as unipay_sor: + plog_id = getID() + biz_date = await get_business_date(sor) + now_str = timestampstr() + plog_data = { + "id": plog_id, + "customerid": customerid, + "channelid": "proxy", + "payment_name": "充值", + "payer_client_ip": "admin_proxy", + "amount_total": amount, + "pay_feerate": 0.0, + "pay_fee": 0.0, + "currency": "CNY", + "payment_status": "1", + "init_timestamp": now_str, + "payed_timestamp": now_str, + "cancel_timestamp": "2000-01-01 00:00:00.001", + "userid": userid + } + await unipay_sor.C('payment_log', plog_data.copy()) + + # Perform recharge accounting + await recharge_accounting( + sor, + customerid, + 'RECHARGE', + plog_id, + biz_date, + amount, + 0.0 + ) + + debug(f'Proxy recharge: user={username}, customerid={customerid}, amount={amount}, operator={userid}') + + orgname = customer.orgname or '' + return { + "widgettype": "Text", + "options": { + "text": f"✅ 代客充值成功 — 已为用户 {username} ({orgname}) 充值 ¥{amount:.2f}", + "color": "#22C55E", + "fontSize": "14px", + "fontWeight": "500" + } + }