feat: 初始化 Wails + React + Go 桌面应用模板
- Wails v2.16.0 + React 19 + TypeScript + Vite 7 - 插件系统(build tag 控制编译,plugin.json 运行时配置) - 支付插件(授权、账号、订单、退款、账单) - 高并发基础设施(任务池、批量处理器、限流器、环形缓冲、管道) - Windows 系统托盘(Win32 API) - CodeGraph 索引 + .memory 记忆包 - 默认图标 assets/icon.png
This commit is contained in:
5
.codegraph/.gitignore
vendored
Normal file
5
.codegraph/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# CodeGraph data files — local to each machine, not for committing.
|
||||
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||
*
|
||||
!.gitignore
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
build/bin
|
||||
node_modules
|
||||
frontend/dist
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
252
.memory/MEMORY.md
Normal file
252
.memory/MEMORY.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# 项目记忆包 — go_win_template
|
||||
|
||||
> 此目录为 AI 辅助开发项目的记忆索引。
|
||||
> 核心工具:**CodeGraph**(代码知识图谱),用于快速检索项目符号和调用关系。
|
||||
> 当 `.codegraph/` 目录存在时,优先使用 CodeGraph 探索代码,而非 grep/find。
|
||||
|
||||
---
|
||||
|
||||
## 📋 项目概述
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 项目类型 | Wails v2 + React + TypeScript 桌面应用 |
|
||||
| 目标平台 | Windows (amd64) |
|
||||
| 框架版本 | Wails v2.16.0 |
|
||||
| 前端框架 | React 19 + Vite 7 + TypeScript 5 |
|
||||
| Go 版本 | Go 1.25.0 |
|
||||
|
||||
### 技术栈
|
||||
- **后端**: Go 原生,Wails IPC (进程内通信,不走 HTTP)
|
||||
- **前端**: React + TypeScript + Vite
|
||||
- **系统托盘**: Windows Win32 API (`shell32.dll::Shell_NotifyIconA`)
|
||||
- **插件系统**: build tag 控制编译,`config/plugin.json` 运行时配置
|
||||
- **支付模块**: HTTP 客户端 + 自动 token 续期 + HMAC 签名
|
||||
- **高并发**: 任务池、批量处理器、限流器、环形缓冲、管道
|
||||
|
||||
---
|
||||
|
||||
## 🕸️ CodeGraph 使用指南
|
||||
|
||||
### 安装状态
|
||||
```
|
||||
已全局安装: @colbymchenry/codegraph@1.6.0
|
||||
项目索引: .codegraph/ (25 files, 431 nodes, 947 edges)
|
||||
MCP 集成: opencode (已配置)
|
||||
```
|
||||
|
||||
### 核心命令
|
||||
|
||||
```bash
|
||||
# 初始化/重建索引
|
||||
codegraph init [path]
|
||||
|
||||
# 查看项目文件结构
|
||||
codegraph files
|
||||
|
||||
# 查看索引状态和统计
|
||||
codegraph status
|
||||
|
||||
# 搜索符号
|
||||
codegraph query <search_term>
|
||||
|
||||
# 探索代码区域(源码 + 调用路径)—— 最常用的命令
|
||||
codegraph explore "<topic>"
|
||||
|
||||
# 构建任务上下文(相关符号、关系、代码块)
|
||||
codegraph context "<task>"
|
||||
|
||||
# 查看单个符号的源码和调用链
|
||||
codegraph node <symbol_name>
|
||||
|
||||
# 查找谁调用了某个函数
|
||||
codegraph callers <symbol>
|
||||
|
||||
# 查找某个函数调用了什么
|
||||
codegraph callees <symbol>
|
||||
|
||||
# 分析修改某符号的影响范围
|
||||
codegraph impact <symbol>
|
||||
|
||||
# 查找受影响的测试文件
|
||||
codegraph affected [files...]
|
||||
|
||||
# 更新索引(增量同步)
|
||||
codegraph sync
|
||||
```
|
||||
|
||||
### 典型工作流
|
||||
|
||||
```bash
|
||||
# 开发新功能前,先构建上下文
|
||||
codegraph context "实现新的插件功能"
|
||||
|
||||
# 查看关键入口函数的完整调用链
|
||||
codegraph node "App.Startup"
|
||||
|
||||
# 探索某个模块的架构
|
||||
codegraph explore "payment service architecture"
|
||||
|
||||
# 修改代码前确认影响范围
|
||||
codegraph impact "CreateUser"
|
||||
```
|
||||
|
||||
### 在 AI 对话中的使用
|
||||
|
||||
当需要理解或修改代码时,**优先使用 CodeGraph 而不是 grep**:
|
||||
|
||||
```bash
|
||||
# ❌ 不要这样(慢且碎片化)
|
||||
grep -r "FunctionName" --include="*.go" .
|
||||
|
||||
# ✅ 应该这样(精准且带上下文)
|
||||
codegraph explore "FunctionName"
|
||||
codegraph node "FunctionName"
|
||||
codegraph callees "FunctionName"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
go_win_template/
|
||||
├── main.go # 入口:wails.Run + 插件启动
|
||||
├── app.go # IPC 接口层:基础业务 + 支付接口
|
||||
├── wails.json # Wails 配置
|
||||
├── go.mod / go.sum
|
||||
│
|
||||
├── config/
|
||||
│ └── plugin.json # 插件开关配置(运行时生效)
|
||||
│
|
||||
├── assets/icon.png # 应用图标
|
||||
├── trayicons/icon.png # 托盘图标(Wails 标准路径)
|
||||
│
|
||||
├── internal/
|
||||
│ ├── app/app.go # 全局上下文单例
|
||||
│ ├── bus/bus.go # 事件总线(模块间解耦)
|
||||
│ ├── cache/cache.go # TTL 内存缓存
|
||||
│ ├── service/concurrent.go # 高并发:BatchWorker、RateLimiter、RingBuffer、Pipeline
|
||||
│ ├── worker/pool.go # 任务并发池(固定工作协程)
|
||||
│ ├── payment/
|
||||
│ │ ├── client.go # HTTP 客户端:自动 token 续期 + HMAC 签名
|
||||
│ │ └── service.go # 支付业务层(登录、查订单、退款、账单)
|
||||
│ ├── plugins/
|
||||
│ │ ├── plugins.go # 插件接口 + 全局注册中心
|
||||
│ │ ├── loader.go # 配置文件加载器(支持环境变量覆盖)
|
||||
│ │ ├── iconfont_impl.go # build tag: iconfont
|
||||
│ │ ├── concurrent_impl.go # build tag: concurrent
|
||||
│ │ ├── ai_impl.go # build tag: ai
|
||||
│ │ └── payment_impl.go # build tag: payment
|
||||
│ └── tray/win/tray.go # Windows 系统托盘(Win32 API)
|
||||
│
|
||||
├── frontend/ # React + TypeScript + Vite
|
||||
│ ├── src/App.tsx # 主页面(用户管理示例)
|
||||
│ ├── src/main.tsx # React 入口
|
||||
│ ├── wailsjs/ # Go → JS 绑定(自动生成)
|
||||
│ └── vite.config.ts
|
||||
│
|
||||
├── .codegraph/ # CodeGraph 索引(自动生成,勿手动编辑)
|
||||
│ └── codegraph.db
|
||||
│
|
||||
└── .memory/ # 本记忆包
|
||||
└── MEMORY.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 插件系统
|
||||
|
||||
### 插件列表
|
||||
|
||||
| 插件名 | Build Tag | 默认状态 | 功能 |
|
||||
|--------|-----------|----------|------|
|
||||
| `concurrent` | `-tags concurrent` | ✅ 启用 | 高并发工具集 |
|
||||
| `payment` | `-tags payment` | ✅ 启用 | 支付授权与订单管理 |
|
||||
| `iconfont` | `-tags iconfont` | ❌ 禁用 | 阿里IconFont图标库 |
|
||||
| `ai` | `-tags ai` | ❌ 禁用 | AI大模型集成 |
|
||||
|
||||
### 运行时配置(无需重新编译)
|
||||
|
||||
编辑 `config/plugin.json` 即可切换插件开关:
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"payment": {
|
||||
"enabled": true,
|
||||
"params": { "app_id": "...", "api_key": "...", "base_url": "..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 编译时启用(完全排除二进制)
|
||||
|
||||
```bash
|
||||
# 仅基础功能
|
||||
wails build
|
||||
|
||||
# 加支付插件
|
||||
wails build -tags payment
|
||||
|
||||
# 加 AI 插件
|
||||
wails build -tags ai
|
||||
|
||||
# 全部启用
|
||||
wails build -tags "payment,ai,iconfont"
|
||||
|
||||
# 环境变量覆盖
|
||||
PLUGIN_AI=true wails build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💰 支付接口(IPC — 前端可调用)
|
||||
|
||||
| Go 方法 | 说明 |
|
||||
|---------|------|
|
||||
| `PayLogin()` | 登录授权,获取 access_token |
|
||||
| `PayCheckAuth()` | 查询授权状态 |
|
||||
| `PayRefreshToken()` | 主动刷新令牌 |
|
||||
| `PayGetAccountInfo()` | 获取账号信息 |
|
||||
| `PayQueryOrder(orderID)` | 查订单 |
|
||||
| `PayQueryOrderByOutNo(outTradeNo)` | 按商户号查订单 |
|
||||
| `PayRequestRefund(orderID, amount, reason)` | 申请退款 |
|
||||
| `PayGetBill(page, pageSize)` | 分页账单 |
|
||||
| `PayGetBillByDate(start, end)` | 按日期查账单 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 开发命令
|
||||
|
||||
```bash
|
||||
# 开发模式(热更新,自动选端口)
|
||||
wails dev
|
||||
|
||||
# 构建 Windows exe
|
||||
wails build
|
||||
|
||||
# 重新生成前端 JS 绑定(新增 IPC 方法后执行)
|
||||
wails generate module
|
||||
|
||||
# 更新 CodeGraph 索引
|
||||
codegraph sync
|
||||
|
||||
# 查看项目结构索引
|
||||
codegraph files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 重要注意事项
|
||||
|
||||
1. **端口冲突**: Wails dev 模式 Vite 自动选可用端口(`strictPort: false`),多实例不冲突
|
||||
2. **生产模式**: 静态资源打包进 exe,无端口暴露,用户无法直接访问端口
|
||||
3. **托盘图标**: Wails v2 标准路径 `trayicons/icon.png`,名称即 ID(`Image: "icon"`)
|
||||
4. **支付插件**: 需在 `config/plugin.json` 的 `params` 中配置 `app_id`、`api_key`、`base_url`
|
||||
5. **CodeGraph**: 索引存储在 `.codegraph/codegraph.db`,修改代码后运行 `codegraph sync` 同步
|
||||
|
||||
---
|
||||
|
||||
*记忆包创建: 2026-09-17*
|
||||
*CodeGraph 索引: 25 files, 431 nodes, 947 edges*
|
||||
19
README.md
Normal file
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# README
|
||||
|
||||
## About
|
||||
|
||||
This is the official Wails React-TS template.
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
|
||||
## Live Development
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
|
||||
## Building
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
322
app.go
Normal file
322
app.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go_win_template/internal/bus"
|
||||
"go_win_template/internal/cache"
|
||||
"go_win_template/internal/plugins"
|
||||
"go_win_template/internal/service"
|
||||
"go_win_template/internal/worker"
|
||||
)
|
||||
|
||||
// App 主应用结构体
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
users []User
|
||||
nextID atomic.Int64
|
||||
userCache *cache.Cache
|
||||
eventBus *bus.EventBus
|
||||
batchWorker *service.BatchWorker
|
||||
taskPool *worker.Pool
|
||||
requestCount atomic.Int64
|
||||
paymentSvc paymentService
|
||||
}
|
||||
|
||||
// paymentService 支付服务接口(定义在 app 层,不与 payment 包直接耦合)
|
||||
// payment 插件在启动时通过类型断言注入此接口
|
||||
type paymentService interface {
|
||||
PayLogin() (map[string]any, error)
|
||||
PayCheckAuth() (map[string]any, error)
|
||||
PayRefresh() (map[string]any, error)
|
||||
PayGetAccountInfo() (map[string]any, error)
|
||||
PayQueryOrder(orderID string) (map[string]any, error)
|
||||
PayQueryOrderByOutNo(outTradeNo string) (map[string]any, error)
|
||||
PayRequestRefund(req map[string]any) (map[string]any, error)
|
||||
PayGetBill(page, pageSize int) (map[string]any, error)
|
||||
PayGetBillByDate(startDate, endDate string) (map[string]any, error)
|
||||
}
|
||||
|
||||
// Response 统一响应
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// User 用户模型
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 初始化
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{
|
||||
users: make([]User, 0),
|
||||
userCache: cache.NewCache(cache.WithTTL(10*time.Minute), cache.WithCleanupInterval(1*time.Minute)),
|
||||
eventBus: bus.NewEventBus(),
|
||||
batchWorker: service.NewBatchWorker(8),
|
||||
taskPool: worker.NewPool(worker.Options{Workers: 4, BufSize: 500}),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
fmt.Println("[app] startup complete")
|
||||
|
||||
// 注入支付服务
|
||||
a.injectPaymentService()
|
||||
}
|
||||
|
||||
func (a *App) injectPaymentService() {
|
||||
payP := plugins.Get("payment")
|
||||
if payP == nil {
|
||||
fmt.Println("[app] payment plugin not loaded (use -tags payment to enable)")
|
||||
return
|
||||
}
|
||||
if getter, ok := payP.(interface{ GetPaymentService() any }); ok {
|
||||
if svc, ok := getter.GetPaymentService().(paymentService); ok {
|
||||
a.paymentSvc = svc
|
||||
fmt.Println("[app] payment service injected successfully")
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Println("[app] payment plugin found but service type mismatch")
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// IPC 接口 — 基础功能
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
func (a *App) GetUserList() []User {
|
||||
a.requestCount.Add(1)
|
||||
if items, ok := a.userCache.Get("user_list"); ok {
|
||||
if cached, ok := items.([]User); ok {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
return a.users
|
||||
}
|
||||
|
||||
func (a *App) CreateUser(name, email string) *Response {
|
||||
if name == "" || email == "" {
|
||||
return &Response{Code: 400, Message: "名称和邮箱不能为空"}
|
||||
}
|
||||
id := a.nextID.Add(1)
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
user := User{ID: id, Name: name, Email: email, CreatedAt: now}
|
||||
a.users = append(a.users, user)
|
||||
a.userCache.Set("user_list", a.users)
|
||||
a.eventBus.PublishAsync("user:created", user)
|
||||
return &Response{Code: 0, Message: "success", Data: user}
|
||||
}
|
||||
|
||||
func (a *App) ClearUsers() int {
|
||||
count := len(a.users)
|
||||
a.users = make([]User, 0)
|
||||
a.userCache.Delete("user_list")
|
||||
return count
|
||||
}
|
||||
|
||||
func (a *App) Stats() map[string]interface{} {
|
||||
running, total, failed := a.taskPool.Stats()
|
||||
br, bt, bs, bf := a.batchWorker.Stats()
|
||||
return map[string]interface{}{
|
||||
"total_users": len(a.users),
|
||||
"cache_items": a.userCache.Len(),
|
||||
"requests": a.requestCount.Load(),
|
||||
"pool_running": running,
|
||||
"pool_total": total,
|
||||
"pool_failed": failed,
|
||||
"batch_running": br,
|
||||
"batch_total": bt,
|
||||
"batch_success": bs,
|
||||
"batch_failed": bf,
|
||||
"plugins": plugins.List(),
|
||||
"plugin_count": plugins.Count(),
|
||||
"payment_loaded": a.paymentSvc != nil,
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// IPC 接口 — 支付功能
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
func (a *App) PayLogin() *Response {
|
||||
if a.paymentSvc == nil {
|
||||
return &Response{Code: 503, Message: "支付插件未加载,使用 -tags payment 构建或检查 plugin.json"}
|
||||
}
|
||||
data, err := a.paymentSvc.PayLogin()
|
||||
if err != nil {
|
||||
return &Response{Code: 500, Message: err.Error()}
|
||||
}
|
||||
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||
}
|
||||
|
||||
func (a *App) PayCheckAuth() *Response {
|
||||
if a.paymentSvc == nil {
|
||||
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||
}
|
||||
data, err := a.paymentSvc.PayCheckAuth()
|
||||
if err != nil {
|
||||
return &Response{Code: 500, Message: err.Error()}
|
||||
}
|
||||
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||
}
|
||||
|
||||
func (a *App) PayRefreshToken() *Response {
|
||||
if a.paymentSvc == nil {
|
||||
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||
}
|
||||
data, err := a.paymentSvc.PayRefresh()
|
||||
if err != nil {
|
||||
return &Response{Code: 500, Message: err.Error()}
|
||||
}
|
||||
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||
}
|
||||
|
||||
func (a *App) PayGetAccountInfo() *Response {
|
||||
if a.paymentSvc == nil {
|
||||
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||
}
|
||||
data, err := a.paymentSvc.PayGetAccountInfo()
|
||||
if err != nil {
|
||||
return &Response{Code: 500, Message: err.Error()}
|
||||
}
|
||||
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||
}
|
||||
|
||||
func (a *App) PayQueryOrder(orderID string) *Response {
|
||||
if a.paymentSvc == nil {
|
||||
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||
}
|
||||
data, err := a.paymentSvc.PayQueryOrder(orderID)
|
||||
if err != nil {
|
||||
return &Response{Code: 500, Message: err.Error()}
|
||||
}
|
||||
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||
}
|
||||
|
||||
func (a *App) PayQueryOrderByOutNo(outTradeNo string) *Response {
|
||||
if a.paymentSvc == nil {
|
||||
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||
}
|
||||
data, err := a.paymentSvc.PayQueryOrderByOutNo(outTradeNo)
|
||||
if err != nil {
|
||||
return &Response{Code: 500, Message: err.Error()}
|
||||
}
|
||||
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||
}
|
||||
|
||||
func (a *App) PayRequestRefund(orderID string, refundAmt float64, reason string) *Response {
|
||||
if a.paymentSvc == nil {
|
||||
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||
}
|
||||
data, err := a.paymentSvc.PayRequestRefund(map[string]any{
|
||||
"order_id": orderID,
|
||||
"refund_amt": refundAmt,
|
||||
"reason": reason,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{Code: 500, Message: err.Error()}
|
||||
}
|
||||
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||
}
|
||||
|
||||
func (a *App) PayGetBill(page, pageSize int) *Response {
|
||||
if a.paymentSvc == nil {
|
||||
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||
}
|
||||
data, err := a.paymentSvc.PayGetBill(page, pageSize)
|
||||
if err != nil {
|
||||
return &Response{Code: 500, Message: err.Error()}
|
||||
}
|
||||
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||
}
|
||||
|
||||
func (a *App) PayGetBillByDate(startDate, endDate string) *Response {
|
||||
if a.paymentSvc == nil {
|
||||
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||
}
|
||||
data, err := a.paymentSvc.PayGetBillByDate(startDate, endDate)
|
||||
if err != nil {
|
||||
return &Response{Code: 500, Message: err.Error()}
|
||||
}
|
||||
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 高并发批量处理
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
func (a *App) BatchCreateUsers(names, emails []string) *Response {
|
||||
if len(names) != len(emails) || len(names) == 0 {
|
||||
return &Response{Code: 400, Message: "names 和 emails 长度需一致且不为空"}
|
||||
}
|
||||
results := a.batchWorker.ProcessConcurrent(a.ctx, createBatchItems(names, emails), func(ctx context.Context, item any) (any, error) {
|
||||
bp := item.(batchItem)
|
||||
id := a.nextID.Add(1)
|
||||
user := User{ID: id, Name: bp.name, Email: bp.email, CreatedAt: time.Now().Format("2006-01-02 15:04:05")}
|
||||
a.users = append(a.users, user)
|
||||
return user, nil
|
||||
})
|
||||
a.userCache.Set("user_list", a.users)
|
||||
a.eventBus.PublishAsync("users:batch_created", results)
|
||||
return &Response{
|
||||
Code: 0,
|
||||
Message: fmt.Sprintf("成功 %d 条,失败 %d 条", len(results.Success), len(results.Fail)),
|
||||
Data: map[string]any{"success": len(results.Success), "failed": len(results.Fail)},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) SubmitTask(taskName, taskData string) *Response {
|
||||
job := &exampleJob{name: taskName, data: taskData}
|
||||
if !a.taskPool.Submit(job) {
|
||||
return &Response{Code: 503, Message: "任务队列已满,请稍后重试"}
|
||||
}
|
||||
return &Response{Code: 0, Message: "任务已入队", Data: map[string]any{"task": taskName}}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 内部类型
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
type batchItem struct{ name, email string }
|
||||
|
||||
func createBatchItems(names, emails []string) []any {
|
||||
items := make([]any, len(names))
|
||||
for i := range names {
|
||||
items[i] = batchItem{name: names[i], email: emails[i]}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
type exampleJob struct{ name, data string }
|
||||
|
||||
func (j *exampleJob) JobID() string { return j.name }
|
||||
|
||||
func (j *exampleJob) Execute(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// asString 安全地从 map 中提取字符串字段
|
||||
func asString(m map[string]any, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 477 B |
35
build/README.md
Normal file
35
build/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Build Directory
|
||||
|
||||
The build directory is used to house all the build files and assets for your application.
|
||||
|
||||
The structure is:
|
||||
|
||||
* bin - Output directory
|
||||
* darwin - macOS specific files
|
||||
* windows - Windows specific files
|
||||
|
||||
## Mac
|
||||
|
||||
The `darwin` directory holds files specific to Mac builds.
|
||||
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||
and
|
||||
build with `wails build`.
|
||||
|
||||
The directory contains the following files:
|
||||
|
||||
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
|
||||
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
|
||||
|
||||
## Windows
|
||||
|
||||
The `windows` directory contains the manifest and rc files used when building with `wails build`.
|
||||
These may be customised for your application. To return these files to the default state, simply delete them and
|
||||
build with `wails build`.
|
||||
|
||||
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
|
||||
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
|
||||
will be created using the `appicon.png` file in the build directory.
|
||||
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
|
||||
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
|
||||
as well as the application itself (right click the exe -> properties -> details)
|
||||
- `wails.exe.manifest` - The main application manifest file.
|
||||
BIN
build/appicon.png
Normal file
BIN
build/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
68
build/darwin/Info.dev.plist
Normal file
68
build/darwin/Info.dev.plist
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{safeBundleID .Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
63
build/darwin/Info.plist
Normal file
63
build/darwin/Info.plist
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{safeBundleID .Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
build/windows/icon.ico
Normal file
BIN
build/windows/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
15
build/windows/info.json
Normal file
15
build/windows/info.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fixed": {
|
||||
"file_version": "{{.Info.ProductVersion}}"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||
"CompanyName": "{{.Info.CompanyName}}",
|
||||
"FileDescription": "{{.Info.ProductName}}",
|
||||
"LegalCopyright": "{{.Info.Copyright}}",
|
||||
"ProductName": "{{.Info.ProductName}}",
|
||||
"Comments": "{{.Info.Comments}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
122
build/windows/installer/project.nsi
Normal file
122
build/windows/installer/project.nsi
Normal file
@@ -0,0 +1,122 @@
|
||||
Unicode true
|
||||
|
||||
####
|
||||
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||
## mentioned underneath.
|
||||
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||
## from outside of Wails for debugging and development of the installer.
|
||||
##
|
||||
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||
## > wails build --target windows/amd64 --nsis
|
||||
## Then you can call makensis on this file with specifying the path to your binary:
|
||||
## For a AMD64 only installer:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||
## For a ARM64 only installer:
|
||||
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||
## For a installer with both architectures:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||
####
|
||||
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
####
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
####
|
||||
## Include the wails tools
|
||||
####
|
||||
!include "wails_tools.nsh"
|
||||
|
||||
# The version information for this two must consist of 4 parts
|
||||
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||
|
||||
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||
ManifestDPIAware true
|
||||
|
||||
!include "MUI.nsh"
|
||||
|
||||
!define MUI_ICON "..\icon.ico"
|
||||
!define MUI_UNICON "..\icon.ico"
|
||||
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||
|
||||
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||
|
||||
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||
#!uninstfinalize 'signtool --file "%1"'
|
||||
#!finalize 'signtool --file "%1"'
|
||||
|
||||
Name "${INFO_PRODUCTNAME}"
|
||||
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||
!ifdef WAILS_INSTALL_SCOPE
|
||||
!if "${WAILS_INSTALL_SCOPE}" == "user"
|
||||
InstallDir "$LOCALAPPDATA\Programs\${INFO_PRODUCTNAME}"
|
||||
!else
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
|
||||
!endif
|
||||
!else
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
|
||||
!endif # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
ShowInstDetails show # This will always show the installation details.
|
||||
|
||||
Function .onInit
|
||||
!insertmacro wails.checkArchitecture
|
||||
FunctionEnd
|
||||
|
||||
Section
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
!insertmacro wails.webview2runtime
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!insertmacro wails.files
|
||||
|
||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
|
||||
!insertmacro wails.associateFiles
|
||||
!insertmacro wails.associateCustomProtocols
|
||||
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
|
||||
Section "uninstall"
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||
|
||||
RMDir /r $INSTDIR
|
||||
|
||||
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||
|
||||
!insertmacro wails.unassociateFiles
|
||||
!insertmacro wails.unassociateCustomProtocols
|
||||
|
||||
!insertmacro wails.deleteUninstaller
|
||||
SectionEnd
|
||||
284
build/windows/installer/wails_tools.nsh
Normal file
284
build/windows/installer/wails_tools.nsh
Normal file
@@ -0,0 +1,284 @@
|
||||
# DO NOT EDIT - Generated automatically by `wails build`
|
||||
|
||||
!include "x64.nsh"
|
||||
!include "WinVer.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "{{.Name}}"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
!endif
|
||||
!ifndef UNINST_KEY_NAME
|
||||
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
!endif
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||
|
||||
!ifndef REQUEST_EXECUTION_LEVEL
|
||||
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||
!endif
|
||||
|
||||
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!ifdef ARG_WAILS_AMD64_BINARY
|
||||
!define SUPPORTS_AMD64
|
||||
!endif
|
||||
|
||||
!ifdef ARG_WAILS_ARM64_BINARY
|
||||
!define SUPPORTS_ARM64
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_AMD64
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "amd64_arm64"
|
||||
!else
|
||||
!define ARCH "amd64"
|
||||
!endif
|
||||
!else
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "arm64"
|
||||
!else
|
||||
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!macro wails.checkArchitecture
|
||||
!ifndef WAILS_WIN10_REQUIRED
|
||||
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||
!endif
|
||||
|
||||
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||
!endif
|
||||
|
||||
${If} ${AtLeastWin10}
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
IfSilent silentArch notSilentArch
|
||||
silentArch:
|
||||
SetErrorLevel 65
|
||||
Abort
|
||||
notSilentArch:
|
||||
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||
Quit
|
||||
${else}
|
||||
IfSilent silentWin notSilentWin
|
||||
silentWin:
|
||||
SetErrorLevel 64
|
||||
Abort
|
||||
notSilentWin:
|
||||
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
!macro wails.files
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro wails.writeUninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
!ifdef WAILS_INSTALL_SCOPE
|
||||
!if "${WAILS_INSTALL_SCOPE}" == "user"
|
||||
WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||
WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||
WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||
WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||
!else
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||
!endif
|
||||
!else
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||
!endif
|
||||
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
IntFmt $0 "0x%08X" $0
|
||||
|
||||
!ifdef WAILS_INSTALL_SCOPE
|
||||
!if "${WAILS_INSTALL_SCOPE}" == "user"
|
||||
WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||
!else
|
||||
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||
!endif
|
||||
!else
|
||||
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro wails.deleteUninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
|
||||
!ifdef WAILS_INSTALL_SCOPE
|
||||
!if "${WAILS_INSTALL_SCOPE}" == "user"
|
||||
DeleteRegKey HKCU "${UNINST_KEY}"
|
||||
!else
|
||||
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||
!endif
|
||||
!else
|
||||
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro wails.setShellContext
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||
SetShellVarContext all
|
||||
${else}
|
||||
SetShellVarContext current
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
# Install webview2 by launching the bootstrapper
|
||||
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||
!macro wails.webview2runtime
|
||||
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||
!endif
|
||||
|
||||
SetRegView 64
|
||||
# If the admin key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
SetDetailsPrint both
|
||||
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||
SetDetailsPrint listonly
|
||||
|
||||
InitPluginsDir
|
||||
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||
|
||||
SetDetailsPrint both
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||
|
||||
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||
!macroend
|
||||
|
||||
!macro wails.associateFiles
|
||||
; Create file associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
File "..\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateFiles
|
||||
; Delete app associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||
|
||||
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
!macroend
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||
{{end}}
|
||||
!macroend
|
||||
15
build/windows/wails.exe.manifest
Normal file
15
build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
27
config/plugin.json
Normal file
27
config/plugin.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"plugins": {
|
||||
"iconfont": {
|
||||
"enabled": false,
|
||||
"description": "阿里IconFont图标库,支持在React中使用字体图标"
|
||||
},
|
||||
"ai": {
|
||||
"enabled": false,
|
||||
"description": "AI大模型集成,支持对话、分析等功能"
|
||||
},
|
||||
"concurrent": {
|
||||
"enabled": true,
|
||||
"description": "高并发工具集,包含任务池、批量处理、限流器等"
|
||||
},
|
||||
"payment": {
|
||||
"enabled": true,
|
||||
"description": "支付授权与订单管理,支持登录、查授权、查账号、查订单、退款、账单",
|
||||
"params": {
|
||||
"app_id": "your_app_id",
|
||||
"mch_id": "",
|
||||
"api_key": "your_api_key",
|
||||
"base_url": "https://api.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>go_win_template</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="./src/main.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1847
frontend/package-lock.json
generated
Normal file
1847
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
1
frontend/package.json.md5
Normal file
1
frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
||||
ebabc2ea396231dd6d5148053c60fb95
|
||||
134
frontend/src/App.css
Normal file
134
frontend/src/App.css
Normal file
@@ -0,0 +1,134 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f0f2f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 16px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 8px 20px;
|
||||
background: #1677ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #4096ff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 6px 14px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 12px;
|
||||
color: #52c41a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 24px;
|
||||
}
|
||||
109
frontend/src/App.tsx
Normal file
109
frontend/src/App.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useState } from 'react';
|
||||
import './App.css';
|
||||
import {
|
||||
GetUserList,
|
||||
CreateUser,
|
||||
} from '../wailsjs/go/main/App';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const loadUsers = async () => {
|
||||
const result = await GetUserList();
|
||||
setUsers(result as User[]);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!name || !email) {
|
||||
setMessage('请输入名称和邮箱');
|
||||
return;
|
||||
}
|
||||
const result = await CreateUser(name, email);
|
||||
if (result.code === 0) {
|
||||
setMessage('创建成功');
|
||||
setName('');
|
||||
setEmail('');
|
||||
await loadUsers();
|
||||
} else {
|
||||
setMessage(result.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="header">
|
||||
<h1>Wails + React + Go</h1>
|
||||
<p>Windows 桌面应用框架示例</p>
|
||||
</header>
|
||||
|
||||
<main className="main">
|
||||
<section className="card">
|
||||
<h2>创建用户</h2>
|
||||
<div className="form-row">
|
||||
<input
|
||||
className="input"
|
||||
placeholder="名称"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
placeholder="邮箱"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<button className="btn-primary" onClick={handleCreate}>
|
||||
创建
|
||||
</button>
|
||||
</div>
|
||||
{message && <p className="message">{message}</p>}
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<div className="card-header">
|
||||
<h2>用户列表</h2>
|
||||
<button className="btn-secondary" onClick={loadUsers}>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
{users.length === 0 ? (
|
||||
<p className="empty">暂无数据</p>
|
||||
) : (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>邮箱</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((u) => (
|
||||
<tr key={u.id}>
|
||||
<td>{u.id}</td>
|
||||
<td>{u.name}</td>
|
||||
<td>{u.email}</td>
|
||||
<td>{u.created_at}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
93
frontend/src/assets/fonts/OFL.txt
Normal file
93
frontend/src/assets/fonts/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/images/logo-universal.png
Normal file
BIN
frontend/src/assets/images/logo-universal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
14
frontend/src/main.tsx
Normal file
14
frontend/src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import './style.css'
|
||||
import App from './App'
|
||||
|
||||
const container = document.getElementById('root')
|
||||
|
||||
const root = createRoot(container!)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
)
|
||||
26
frontend/src/style.css
Normal file
26
frontend/src/style.css
Normal file
@@ -0,0 +1,26 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
31
frontend/tsconfig.json
Normal file
31
frontend/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
frontend/tsconfig.node.json
Normal file
11
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
13
frontend/vite.config.ts
Normal file
13
frontend/vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
// 端口被占用时自动尝试下一个,而不是直接报错退出
|
||||
strictPort: false,
|
||||
// 允许在同一台机器上同时跑多个 wails dev 实例时不冲突
|
||||
cors: true,
|
||||
},
|
||||
})
|
||||
36
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
36
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {main} from '../models';
|
||||
import {context} from '../models';
|
||||
|
||||
export function BatchCreateUsers(arg1:Array<string>,arg2:Array<string>):Promise<main.Response>;
|
||||
|
||||
export function ClearUsers():Promise<number>;
|
||||
|
||||
export function CreateUser(arg1:string,arg2:string):Promise<main.Response>;
|
||||
|
||||
export function GetUserList():Promise<Array<main.User>>;
|
||||
|
||||
export function PayCheckAuth():Promise<main.Response>;
|
||||
|
||||
export function PayGetAccountInfo():Promise<main.Response>;
|
||||
|
||||
export function PayGetBill(arg1:number,arg2:number):Promise<main.Response>;
|
||||
|
||||
export function PayGetBillByDate(arg1:string,arg2:string):Promise<main.Response>;
|
||||
|
||||
export function PayLogin():Promise<main.Response>;
|
||||
|
||||
export function PayQueryOrder(arg1:string):Promise<main.Response>;
|
||||
|
||||
export function PayQueryOrderByOutNo(arg1:string):Promise<main.Response>;
|
||||
|
||||
export function PayRefreshToken():Promise<main.Response>;
|
||||
|
||||
export function PayRequestRefund(arg1:string,arg2:number,arg3:string):Promise<main.Response>;
|
||||
|
||||
export function Startup(arg1:context.Context):Promise<void>;
|
||||
|
||||
export function Stats():Promise<Record<string, any>>;
|
||||
|
||||
export function SubmitTask(arg1:string,arg2:string):Promise<main.Response>;
|
||||
67
frontend/wailsjs/go/main/App.js
Normal file
67
frontend/wailsjs/go/main/App.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function BatchCreateUsers(arg1, arg2) {
|
||||
return window['go']['main']['App']['BatchCreateUsers'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ClearUsers() {
|
||||
return window['go']['main']['App']['ClearUsers']();
|
||||
}
|
||||
|
||||
export function CreateUser(arg1, arg2) {
|
||||
return window['go']['main']['App']['CreateUser'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetUserList() {
|
||||
return window['go']['main']['App']['GetUserList']();
|
||||
}
|
||||
|
||||
export function PayCheckAuth() {
|
||||
return window['go']['main']['App']['PayCheckAuth']();
|
||||
}
|
||||
|
||||
export function PayGetAccountInfo() {
|
||||
return window['go']['main']['App']['PayGetAccountInfo']();
|
||||
}
|
||||
|
||||
export function PayGetBill(arg1, arg2) {
|
||||
return window['go']['main']['App']['PayGetBill'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function PayGetBillByDate(arg1, arg2) {
|
||||
return window['go']['main']['App']['PayGetBillByDate'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function PayLogin() {
|
||||
return window['go']['main']['App']['PayLogin']();
|
||||
}
|
||||
|
||||
export function PayQueryOrder(arg1) {
|
||||
return window['go']['main']['App']['PayQueryOrder'](arg1);
|
||||
}
|
||||
|
||||
export function PayQueryOrderByOutNo(arg1) {
|
||||
return window['go']['main']['App']['PayQueryOrderByOutNo'](arg1);
|
||||
}
|
||||
|
||||
export function PayRefreshToken() {
|
||||
return window['go']['main']['App']['PayRefreshToken']();
|
||||
}
|
||||
|
||||
export function PayRequestRefund(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['PayRequestRefund'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function Startup(arg1) {
|
||||
return window['go']['main']['App']['Startup'](arg1);
|
||||
}
|
||||
|
||||
export function Stats() {
|
||||
return window['go']['main']['App']['Stats']();
|
||||
}
|
||||
|
||||
export function SubmitTask(arg1, arg2) {
|
||||
return window['go']['main']['App']['SubmitTask'](arg1, arg2);
|
||||
}
|
||||
39
frontend/wailsjs/go/models.ts
Normal file
39
frontend/wailsjs/go/models.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export namespace main {
|
||||
|
||||
export class Response {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: any;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Response(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.code = source["code"];
|
||||
this.message = source["message"];
|
||||
this.data = source["data"];
|
||||
}
|
||||
}
|
||||
export class User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new User(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.name = source["name"];
|
||||
this.email = source["email"];
|
||||
this.created_at = source["created_at"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
24
frontend/wailsjs/runtime/package.json
Normal file
24
frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
330
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
330
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
298
frontend/wailsjs/runtime/runtime.js
Normal file
298
frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
38
go.mod
Normal file
38
go.mod
Normal file
@@ -0,0 +1,38 @@
|
||||
module go_win_template
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.16.0
|
||||
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.39.0 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.16.0 => C:\Users\12977\go\pkg\mod
|
||||
83
go.sum
Normal file
83
go.sum
Normal file
@@ -0,0 +1,83 @@
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.16.0 h1:hLYrSHUUq6hRXl3rWT9ZXiyFIgM94nVN9Ns6iEJzxyw=
|
||||
github.com/wailsapp/wails/v2 v2.16.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
48
internal/app/app.go
Normal file
48
internal/app/app.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Instance 全局应用单例,持有 ctx 和托盘引用
|
||||
type Instance struct {
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
name string
|
||||
}
|
||||
|
||||
// App 应用上下文入口
|
||||
type App struct {
|
||||
instance *Instance
|
||||
}
|
||||
|
||||
var singleton *Instance
|
||||
var once sync.Once
|
||||
|
||||
// Init 初始化应用全局上下文(只调用一次)
|
||||
func Init() *App {
|
||||
once.Do(func() {
|
||||
singleton = &Instance{name: "go_win_template"}
|
||||
})
|
||||
return &App{instance: singleton}
|
||||
}
|
||||
|
||||
// GetSingleton 获取全局单例
|
||||
func GetSingleton() *Instance {
|
||||
return singleton
|
||||
}
|
||||
|
||||
// SetContext 设置 Wails context
|
||||
func (s *Instance) SetContext(ctx context.Context) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.ctx = ctx
|
||||
}
|
||||
|
||||
// Context 获取当前 context
|
||||
func (s *Instance) Context() context.Context {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.ctx
|
||||
}
|
||||
62
internal/bus/bus.go
Normal file
62
internal/bus/bus.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Handler 事件处理器类型
|
||||
type Handler func(payload any)
|
||||
|
||||
// EventBus 应用内事件总线,用于模块间解耦通信
|
||||
type EventBus struct {
|
||||
mu sync.RWMutex
|
||||
handlers map[string][]Handler
|
||||
}
|
||||
|
||||
// NewEventBus 创建事件总线实例
|
||||
func NewEventBus() *EventBus {
|
||||
return &EventBus{
|
||||
handlers: make(map[string][]Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe 订阅事件,返回取消函数
|
||||
func (b *EventBus) Subscribe(event string, handler Handler) func() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.handlers[event] = append(b.handlers[event], handler)
|
||||
return func() { b.Unsubscribe(event, handler) }
|
||||
}
|
||||
|
||||
// Unsubscribe 取消订阅
|
||||
func (b *EventBus) Unsubscribe(event string, handler Handler) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
handlers := b.handlers[event]
|
||||
for i, h := range handlers {
|
||||
// 函数不能直接比较,用索引追踪
|
||||
_ = i
|
||||
_ = h
|
||||
// 简化:直接清空该事件所有处理器(实际项目可用 handler ID)
|
||||
b.handlers[event] = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Publish 同步发布事件
|
||||
func (b *EventBus) Publish(event string, payload any) {
|
||||
b.mu.RLock()
|
||||
handlers := make([]Handler, len(b.handlers[event]))
|
||||
copy(handlers, b.handlers[event])
|
||||
b.mu.RUnlock()
|
||||
for _, h := range handlers {
|
||||
if h != nil {
|
||||
h(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PublishAsync 异步发布事件,不阻塞调用方
|
||||
func (b *EventBus) PublishAsync(event string, payload any) {
|
||||
b.Publish(event, payload) // 当前同步调用,后续可扩展为 goroutine
|
||||
}
|
||||
119
internal/cache/cache.go
vendored
Normal file
119
internal/cache/cache.go
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Item 缓存项
|
||||
type Item struct {
|
||||
Value any
|
||||
Expiration int64 // Unix 纳秒,0 表示永不过期
|
||||
}
|
||||
|
||||
// IsExpired 是否已过期
|
||||
func (i *Item) IsExpired() bool {
|
||||
if i.Expiration == 0 {
|
||||
return false
|
||||
}
|
||||
return time.Now().UnixNano() > i.Expiration
|
||||
}
|
||||
|
||||
// Cache 线程安全的内存缓存,支持 TTL
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]*Item
|
||||
ttl time.Duration
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
// Option 缓存配置选项
|
||||
type Option func(*Cache)
|
||||
|
||||
// WithTTL 设置默认过期时间
|
||||
func WithTTL(ttl time.Duration) Option {
|
||||
return func(c *Cache) {
|
||||
c.ttl = ttl
|
||||
}
|
||||
}
|
||||
|
||||
// WithCleanupInterval 设置后台清理间隔
|
||||
func WithCleanupInterval(d time.Duration) Option {
|
||||
return func(c *Cache) {
|
||||
go c.cleanupLoop(d)
|
||||
}
|
||||
}
|
||||
|
||||
// NewCache 创建缓存实例
|
||||
func NewCache(opts ...Option) *Cache {
|
||||
c := &Cache{
|
||||
items: make(map[string]*Item),
|
||||
ttl: 5 * time.Minute,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Set 设置缓存项
|
||||
func (c *Cache) Set(key string, value any) {
|
||||
c.SetWithTTL(key, value, c.ttl)
|
||||
}
|
||||
|
||||
// SetWithTTL 设置缓存项并指定过期时间
|
||||
func (c *Cache) SetWithTTL(key string, value any, ttl time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
exp := int64(0)
|
||||
if ttl > 0 {
|
||||
exp = time.Now().Add(ttl).UnixNano()
|
||||
}
|
||||
c.items[key] = &Item{Value: value, Expiration: exp}
|
||||
}
|
||||
|
||||
// Get 获取缓存项
|
||||
func (c *Cache) Get(key string) (any, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
item, ok := c.items[key]
|
||||
if !ok || item.IsExpired() {
|
||||
return nil, false
|
||||
}
|
||||
return item.Value, true
|
||||
}
|
||||
|
||||
// Delete 删除缓存项
|
||||
func (c *Cache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.items, key)
|
||||
}
|
||||
|
||||
// Len 返回缓存项数量
|
||||
func (c *Cache) Len() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.items)
|
||||
}
|
||||
|
||||
// Clear 清空缓存
|
||||
func (c *Cache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.items = make(map[string]*Item)
|
||||
}
|
||||
|
||||
func (c *Cache) cleanupLoop(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
c.mu.Lock()
|
||||
for k, v := range c.items {
|
||||
if v.IsExpired() {
|
||||
delete(c.items, k)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
272
internal/payment/client.go
Normal file
272
internal/payment/client.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 配置与数据结构
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
// Config 支付平台配置,可接入任意支付网关
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"`
|
||||
MchID string `json:"mch_id"` // 商户号(可选)
|
||||
APIKey string `json:"api_key"` // 签名密钥
|
||||
BaseURL string `json:"base_url"` // 网关地址
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
AutoRefreshToken bool `json:"auto_refresh_token"` // 是否自动续期
|
||||
}
|
||||
|
||||
// Token 授权令牌
|
||||
type Token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
IsExpired func() bool
|
||||
}
|
||||
|
||||
func (t *Token) update(expiresIn int) {
|
||||
t.ExpiresIn = expiresIn
|
||||
t.ExpiresAt = time.Now().Add(time.Duration(expiresIn) * time.Second)
|
||||
t.IsExpired = func() bool {
|
||||
return time.Now().After(t.ExpiresAt.Add(-30 * time.Second)) // 提前30秒判定过期
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// HTTP 客户端(带自动重填和签名)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
// Client 支付 HTTP 客户端
|
||||
type Client struct {
|
||||
mu sync.Mutex
|
||||
cfg Config
|
||||
httpClient *http.Client
|
||||
token *Token
|
||||
tokenMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewClient 创建支付客户端
|
||||
func NewClient(cfg Config) *Client {
|
||||
if cfg.Timeout == 0 {
|
||||
cfg.Timeout = 10 * time.Second
|
||||
}
|
||||
if cfg.BaseURL == "" {
|
||||
cfg.BaseURL = "https://api.example.com"
|
||||
}
|
||||
c := &Client{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{Timeout: cfg.Timeout},
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// GetToken 获取访问令牌
|
||||
func (c *Client) GetToken(ctx context.Context) (*Token, error) {
|
||||
c.tokenMu.RLock()
|
||||
if c.token != nil && !c.token.IsExpired() {
|
||||
c.tokenMu.RUnlock()
|
||||
return c.token, nil
|
||||
}
|
||||
c.tokenMu.RUnlock()
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// double-check
|
||||
c.tokenMu.RLock()
|
||||
if c.token != nil && !c.token.IsExpired() {
|
||||
c.tokenMu.RUnlock()
|
||||
return c.token, nil
|
||||
}
|
||||
c.tokenMu.RUnlock()
|
||||
|
||||
reqURL := fmt.Sprintf("%s/oauth/token", c.cfg.BaseURL)
|
||||
body := map[string]string{
|
||||
"app_id": c.cfg.AppID,
|
||||
"timestamp": fmt.Sprintf("%d", time.Now().Unix()),
|
||||
"nonce": generateNonce(),
|
||||
}
|
||||
reqBody, _ := json.Marshal(body)
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(string(reqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
addSignature(req, body, c.cfg.APIKey)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
} `json:"data"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("get token failed: %s", result.Message)
|
||||
}
|
||||
|
||||
c.token = &Token{}
|
||||
c.token.update(result.Data.ExpiresIn)
|
||||
c.token.AccessToken = result.Data.AccessToken
|
||||
return c.token, nil
|
||||
}
|
||||
|
||||
// RefreshToken 主动刷新令牌
|
||||
func (c *Client) RefreshToken(ctx context.Context) (*Token, error) {
|
||||
reqURL := fmt.Sprintf("%s/oauth/refresh", c.cfg.BaseURL)
|
||||
body := map[string]string{
|
||||
"app_id": c.cfg.AppID,
|
||||
"timestamp": fmt.Sprintf("%d", time.Now().Unix()),
|
||||
}
|
||||
reqBody, _ := json.Marshal(body)
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(string(reqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
addSignature(req, body, c.cfg.APIKey)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
} `json:"data"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("refresh token failed: %s", result.Message)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.token = &Token{}
|
||||
c.token.update(result.Data.ExpiresIn)
|
||||
c.token.AccessToken = result.Data.AccessToken
|
||||
c.mu.Unlock()
|
||||
return c.token, nil
|
||||
}
|
||||
|
||||
// Get 发送 GET 请求(自动携带 token)
|
||||
func (c *Client) Get(ctx context.Context, path string, params map[string]string) ([]byte, error) {
|
||||
token, err := c.GetToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.doRequest(ctx, http.MethodGet, path, params, token.AccessToken, nil)
|
||||
}
|
||||
|
||||
// Post 发送 POST 请求(自动携带 token)
|
||||
func (c *Client) Post(ctx context.Context, path string, params map[string]string, body any) ([]byte, error) {
|
||||
token, err := c.GetToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.doRequest(ctx, http.MethodPost, path, params, token.AccessToken, body)
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, method, path string, params map[string]string, token string, body any) ([]byte, error) {
|
||||
reqURL := c.cfg.BaseURL + path
|
||||
values := url.Values{}
|
||||
for k, v := range params {
|
||||
values.Set(k, v)
|
||||
}
|
||||
if values.Encode() != "" {
|
||||
reqURL += "?" + values.Encode()
|
||||
}
|
||||
|
||||
var reqBody io.Reader
|
||||
var contentType string
|
||||
if body != nil {
|
||||
data, _ := json.Marshal(body)
|
||||
reqBody = strings.NewReader(string(data))
|
||||
contentType = "application/json"
|
||||
}
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, method, reqURL, reqBody)
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %s %s: %w", method, path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 签名辅助
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
func addSignature(req *http.Request, body map[string]string, apiKey string) {
|
||||
signStr := buildSignString(body)
|
||||
sig := hmacSHA256(signStr, apiKey)
|
||||
req.Header.Set("X-Signature", sig)
|
||||
}
|
||||
|
||||
func buildSignString(params map[string]string) string {
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sortStrings(keys)
|
||||
var sb strings.Builder
|
||||
for _, k := range keys {
|
||||
sb.WriteString(k)
|
||||
sb.WriteString("=")
|
||||
sb.WriteString(params[k])
|
||||
sb.WriteString("&")
|
||||
}
|
||||
return strings.TrimRight(sb.String(), "&")
|
||||
}
|
||||
|
||||
func hmacSHA256(data, key string) string {
|
||||
m := hmac.New(sha256.New, []byte(key))
|
||||
m.Write([]byte(data))
|
||||
return fmt.Sprintf("%x", m.Sum(nil))
|
||||
}
|
||||
|
||||
func sortStrings(s []string) {
|
||||
for i := 1; i < len(s); i++ {
|
||||
for j := i; j > 0 && s[j] < s[j-1]; j-- {
|
||||
s[j], s[j-1] = s[j-1], s[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateNonce() string {
|
||||
// 简单 nonce,生产环境建议使用 crypto/rand
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
149
internal/payment/service.go
Normal file
149
internal/payment/service.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Service 支付业务层,内部封装 Client,对外暴露方法
|
||||
type Service struct {
|
||||
client *Client
|
||||
cfg Config
|
||||
}
|
||||
|
||||
// NewService 创建支付服务
|
||||
func NewService(cfg Config) *Service {
|
||||
return &Service{
|
||||
client: NewClient(cfg),
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// GetClient 获取底层 HTTP 客户端(供高级用法)
|
||||
func (s *Service) GetClient() *Client {
|
||||
return s.client
|
||||
}
|
||||
|
||||
// Login 登录授权,获取 access_token
|
||||
func (s *Service) Login(ctx context.Context) (map[string]any, error) {
|
||||
token, err := s.client.GetToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"access_token": token.AccessToken,
|
||||
"expires_in": token.ExpiresIn,
|
||||
"message": "授权成功",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckAuth 查询授权状态
|
||||
func (s *Service) CheckAuth(ctx context.Context) (map[string]any, error) {
|
||||
token, err := s.client.GetToken(ctx)
|
||||
if err != nil {
|
||||
return map[string]any{"success": false, "message": "授权失效,请重新登录"}, err
|
||||
}
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"access_token": token.AccessToken,
|
||||
"expires_in": token.ExpiresIn,
|
||||
"message": "授权有效",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Refresh 主动刷新令牌
|
||||
func (s *Service) Refresh(ctx context.Context) (map[string]any, error) {
|
||||
token, err := s.client.RefreshToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"access_token": token.AccessToken,
|
||||
"expires_in": token.ExpiresIn,
|
||||
"message": "续期成功",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAccountInfo 获取授权账号信息
|
||||
func (s *Service) GetAccountInfo(ctx context.Context) (map[string]any, error) {
|
||||
raw, err := s.client.Get(ctx, "/v1/account/info", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get account info: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// QueryOrder 通过 order_id 查询订单
|
||||
func (s *Service) QueryOrder(ctx context.Context, orderID string) (map[string]any, error) {
|
||||
raw, err := s.client.Get(ctx, "/v1/orders/"+orderID, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query order: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// QueryOrderByOutNo 通过商户订单号查询
|
||||
func (s *Service) QueryOrderByOutNo(ctx context.Context, outTradeNo string) (map[string]any, error) {
|
||||
raw, err := s.client.Get(ctx, "/v1/orders", map[string]string{"out_trade_no": outTradeNo})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query order by out_no: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// RequestRefund 申请退款
|
||||
func (s *Service) RequestRefund(ctx context.Context, req map[string]any) (map[string]any, error) {
|
||||
raw, err := s.client.Post(ctx, "/v1/refunds", nil, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request refund: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// GetBill 分页获取账单
|
||||
func (s *Service) GetBill(ctx context.Context, page, pageSize int) (map[string]any, error) {
|
||||
params := map[string]string{
|
||||
"page": fmt.Sprintf("%d", page),
|
||||
"page_size": fmt.Sprintf("%d", pageSize),
|
||||
}
|
||||
raw, err := s.client.Get(ctx, "/v1/bills", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get bill: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// GetBillByDate 按日期范围查账单
|
||||
func (s *Service) GetBillByDate(ctx context.Context, startDate, endDate string) (map[string]any, error) {
|
||||
params := map[string]string{
|
||||
"start_date": startDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
raw, err := s.client.Get(ctx, "/v1/bills/range", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get bill by date: %w", err)
|
||||
}
|
||||
return parseAPIResponse(raw)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 内部辅助
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
func parseAPIResponse(raw []byte) (map[string]any, error) {
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return nil, fmt.Errorf("api error code=%d msg=%s", result.Code, result.Message)
|
||||
}
|
||||
return map[string]any{"success": true, "data": result.Data, "message": result.Message}, nil
|
||||
}
|
||||
26
internal/plugins/ai_impl.go
Normal file
26
internal/plugins/ai_impl.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build ai
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type aiPlugin struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (p *aiPlugin) Name() string { return "ai" }
|
||||
func (p *aiPlugin) Enabled() bool { return p.enabled }
|
||||
func (p *aiPlugin) Init(ctx context.Context) error {
|
||||
fmt.Println("[ai] initialized")
|
||||
return nil
|
||||
}
|
||||
func (p *aiPlugin) Register(app *App) error { return nil }
|
||||
func (p *aiPlugin) Shutdown() error { return nil }
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&aiPlugin{enabled: cfg.Plugins["ai"].Enabled})
|
||||
}
|
||||
26
internal/plugins/concurrent_impl.go
Normal file
26
internal/plugins/concurrent_impl.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build concurrent
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type concurrentPlugin struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (p *concurrentPlugin) Name() string { return "concurrent" }
|
||||
func (p *concurrentPlugin) Enabled() bool { return p.enabled }
|
||||
func (p *concurrentPlugin) Init(ctx context.Context) error {
|
||||
fmt.Println("[concurrent] initialized")
|
||||
return nil
|
||||
}
|
||||
func (p *concurrentPlugin) Register(app *App) error { return nil }
|
||||
func (p *concurrentPlugin) Shutdown() error { return nil }
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&concurrentPlugin{enabled: cfg.Plugins["concurrent"].Enabled})
|
||||
}
|
||||
26
internal/plugins/iconfont_impl.go
Normal file
26
internal/plugins/iconfont_impl.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build iconfont
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type iconfontPlugin struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (p *iconfontPlugin) Name() string { return "iconfont" }
|
||||
func (p *iconfontPlugin) Enabled() bool { return p.enabled }
|
||||
func (p *iconfontPlugin) Init(ctx context.Context) error {
|
||||
fmt.Println("[iconfont] initialized")
|
||||
return nil
|
||||
}
|
||||
func (p *iconfontPlugin) Register(app *App) error { return nil }
|
||||
func (p *iconfontPlugin) Shutdown() error { return nil }
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&iconfontPlugin{enabled: cfg.Plugins["iconfont"].Enabled})
|
||||
}
|
||||
153
internal/plugins/loader.go
Normal file
153
internal/plugins/loader.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Config 插件配置文件结构
|
||||
type Config struct {
|
||||
Version string `json:"version"`
|
||||
Plugins map[string]PluginOption `json:"plugins"`
|
||||
}
|
||||
|
||||
// PluginOption 单个插件配置项
|
||||
type PluginOption struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Description string `json:"description"`
|
||||
// Params 插件专用参数,由插件自己解析
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// Loader 插件配置加载器
|
||||
type Loader struct {
|
||||
mu sync.RWMutex
|
||||
cfg *Config
|
||||
path string
|
||||
}
|
||||
|
||||
// NewLoader 创建配置加载器
|
||||
func NewLoader(path string) *Loader {
|
||||
return &Loader{path: path}
|
||||
}
|
||||
|
||||
// Load 从文件加载配置
|
||||
func (l *Loader) Load() (*Config, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(l.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 环境变量覆盖优先级最高
|
||||
if v := os.Getenv("PLUGIN_ICONFONT"); v == "false" || v == "0" {
|
||||
tmp := cfg.Plugins["iconfont"]; tmp.Enabled = false; cfg.Plugins["iconfont"] = tmp
|
||||
}
|
||||
if v := os.Getenv("PLUGIN_AI"); v == "true" || v == "1" {
|
||||
tmp := cfg.Plugins["ai"]; tmp.Enabled = true; cfg.Plugins["ai"] = tmp
|
||||
}
|
||||
if v := os.Getenv("PLUGIN_CONCURRENT"); v == "false" || v == "0" {
|
||||
tmp := cfg.Plugins["concurrent"]; tmp.Enabled = false; cfg.Plugins["concurrent"] = tmp
|
||||
}
|
||||
|
||||
l.cfg = &cfg
|
||||
return l.cfg, nil
|
||||
}
|
||||
|
||||
// IsEnabled 查询插件启用状态
|
||||
func (l *Loader) IsEnabled(name string) bool {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
if l.cfg == nil {
|
||||
return false
|
||||
}
|
||||
pc, ok := l.cfg.Plugins[name]
|
||||
return ok && pc.Enabled
|
||||
}
|
||||
|
||||
// GetEnabledNames 返回所有已启用插件名
|
||||
func (l *Loader) GetEnabledNames() []string {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
if l.cfg == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0)
|
||||
for name, pc := range l.cfg.Plugins {
|
||||
if pc.Enabled {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Save 保存配置
|
||||
func (l *Loader) Save(cfg *Config) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(l.path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(l.path, data, 0644)
|
||||
}
|
||||
|
||||
// LoadOrDefault 加载配置,找不到则返回默认值
|
||||
func LoadOrDefault() (*Config, error) {
|
||||
paths := []string{"config/plugin.json", "./plugin.json"}
|
||||
exe, _ := os.Executable()
|
||||
if exe != "" {
|
||||
dir := filepath.Dir(exe)
|
||||
paths = append([]string{
|
||||
filepath.Join(dir, "config", "plugin.json"),
|
||||
filepath.Join(dir, "plugin.json"),
|
||||
}, paths...)
|
||||
}
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
loader := NewLoader(p)
|
||||
if cfg, err := loader.Load(); err == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultConfig(), nil
|
||||
}
|
||||
|
||||
// EnsureConfig 确保配置文件存在
|
||||
func EnsureConfig() error {
|
||||
_, err := os.Stat("config/plugin.json")
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
cfg, _ := LoadOrDefault()
|
||||
loader := NewLoader("config/plugin.json")
|
||||
if mkErr := os.MkdirAll("config", 0755); mkErr != nil {
|
||||
return mkErr
|
||||
}
|
||||
return loader.Save(cfg)
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
Version: "1.0.0",
|
||||
Plugins: map[string]PluginOption{
|
||||
"iconfont": {Enabled: false, Description: "阿里IconFont图标库,支持字体图标"},
|
||||
"ai": {Enabled: false, Description: "AI大模型集成,支持对话、分析等功能"},
|
||||
"concurrent": {Enabled: true, Description: "高并发工具集,任务池、限流器等"},
|
||||
},
|
||||
}
|
||||
}
|
||||
54
internal/plugins/payment_impl.go
Normal file
54
internal/plugins/payment_impl.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Package plugins — payment 插件实现(带 build tag,未启用时不编译)
|
||||
//go:build payment
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go_win_template/internal/payment"
|
||||
)
|
||||
|
||||
// paymentPluginImpl 支付插件
|
||||
type paymentPluginImpl struct {
|
||||
enabled bool
|
||||
svc *payment.Service
|
||||
}
|
||||
|
||||
func (p *paymentPluginImpl) Name() string { return "payment" }
|
||||
func (p *paymentPluginImpl) Enabled() bool { return p.enabled }
|
||||
func (p *paymentPluginImpl) Init(ctx context.Context) error {
|
||||
cfg, _ := LoadOrDefault()
|
||||
pcfg := payment.Config{
|
||||
AppID: getStringParam(cfg, "payment", "app_id"),
|
||||
MchID: getStringParam(cfg, "payment", "mch_id"),
|
||||
APIKey: getStringParam(cfg, "payment", "api_key"),
|
||||
BaseURL: getStringParam(cfg, "payment", "base_url"),
|
||||
AutoRefreshToken: true,
|
||||
}
|
||||
p.svc = payment.NewService(pcfg)
|
||||
fmt.Printf("[payment] initialized, base_url=%s\n", pcfg.BaseURL)
|
||||
return nil
|
||||
}
|
||||
func (p *paymentPluginImpl) Register(app *App) error { return nil }
|
||||
func (p *paymentPluginImpl) Shutdown() error { return nil }
|
||||
|
||||
// GetPaymentService 返回支付服务(any 类型,供 App 层类型断言)
|
||||
func (p *paymentPluginImpl) GetPaymentService() any {
|
||||
return p.svc
|
||||
}
|
||||
|
||||
func init() {
|
||||
cfg, _ := LoadOrDefault()
|
||||
Register(&paymentPluginImpl{enabled: cfg.Plugins["payment"].Enabled})
|
||||
}
|
||||
|
||||
func getStringParam(cfg *Config, plugin, key string) string {
|
||||
if opts, ok := cfg.Plugins[plugin]; ok {
|
||||
if v, ok := opts.Params[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
150
internal/plugins/plugins.go
Normal file
150
internal/plugins/plugins.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Package plugins 插件系统核心接口与注册中心
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Plugin 插件接口,所有插件必须实现
|
||||
type Plugin interface {
|
||||
Name() string
|
||||
Init(ctx context.Context) error
|
||||
Register(app *App) error
|
||||
Shutdown() error
|
||||
Enabled() bool
|
||||
}
|
||||
|
||||
// App 插件可见的应用上下文
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
config map[string]interface{}
|
||||
}
|
||||
|
||||
// NewApp 创建应用上下文
|
||||
func NewApp(ctx context.Context, config map[string]interface{}) *App {
|
||||
return &App{ctx: ctx, config: config}
|
||||
}
|
||||
|
||||
// Registry 插件注册中心(线程安全)
|
||||
type Registry struct {
|
||||
mu sync.Mutex
|
||||
plugins map[string]Plugin
|
||||
}
|
||||
|
||||
var globalRegistry *Registry
|
||||
var once sync.Once
|
||||
|
||||
func getGlobalRegistry() *Registry {
|
||||
once.Do(func() {
|
||||
globalRegistry = &Registry{plugins: make(map[string]Plugin)}
|
||||
})
|
||||
return globalRegistry
|
||||
}
|
||||
|
||||
// Register 全局注册插件(由各插件 build.go 的 init() 调用)
|
||||
func Register(p Plugin) {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.plugins[p.Name()] = p
|
||||
log.Printf("[plugins] registered: %s (enabled=%v)", p.Name(), p.Enabled())
|
||||
}
|
||||
|
||||
// Get 获取已注册插件
|
||||
func Get(name string) Plugin {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.plugins[name]
|
||||
}
|
||||
|
||||
// List 列出所有已注册插件名
|
||||
func List() []string {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
names := make([]string, 0, len(r.plugins))
|
||||
for name := range r.plugins {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// Count 返回已注册插件数
|
||||
func Count() int {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return len(r.plugins)
|
||||
}
|
||||
|
||||
// Start 启动所有已启用的插件
|
||||
func Start(ctx context.Context) error {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for name, p := range r.plugins {
|
||||
if !p.Enabled() {
|
||||
log.Printf("[plugins] skip disabled: %s", name)
|
||||
continue
|
||||
}
|
||||
if err := p.Init(ctx); err != nil {
|
||||
log.Printf("[plugins] init %s failed: %v", name, err)
|
||||
continue
|
||||
}
|
||||
if err := p.Register(NewApp(ctx, nil)); err != nil {
|
||||
log.Printf("[plugins] register %s failed: %v", name, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[plugins] started: %s", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 关闭所有插件
|
||||
func Shutdown() error {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for name, p := range r.plugins {
|
||||
if err := p.Shutdown(); err != nil {
|
||||
log.Printf("[plugins] shutdown %s error: %v", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnabledPlugins 返回所有已启用插件列表
|
||||
func EnabledPlugins() []Plugin {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
result := make([]Plugin, 0)
|
||||
for _, p := range r.plugins {
|
||||
if p.Enabled() {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PrintStatus 打印当前插件状态(调试用)
|
||||
func PrintStatus() {
|
||||
r := getGlobalRegistry()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
fmt.Println("[plugins] current status:")
|
||||
for name, p := range r.plugins {
|
||||
status := "disabled"
|
||||
if p.Enabled() {
|
||||
status = "enabled"
|
||||
}
|
||||
fmt.Printf(" - %s: %s\n", name, status)
|
||||
}
|
||||
if len(r.plugins) == 0 {
|
||||
fmt.Println(" (no plugins registered — check build tags)")
|
||||
}
|
||||
}
|
||||
369
internal/service/concurrent.go
Normal file
369
internal/service/concurrent.go
Normal file
@@ -0,0 +1,369 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BatchResult 批量任务结果
|
||||
type BatchResult struct {
|
||||
Success []any
|
||||
Fail []BatchError
|
||||
}
|
||||
|
||||
// BatchError 批量任务失败记录
|
||||
type BatchError struct {
|
||||
Index int
|
||||
Item any
|
||||
Err error
|
||||
}
|
||||
|
||||
// BatchWorker 批量处理器,适合文件处理、数据转换等高并发场景
|
||||
type BatchWorker struct {
|
||||
mu sync.Mutex
|
||||
running atomic.Int64
|
||||
total atomic.Int64
|
||||
success atomic.Int64
|
||||
failed atomic.Int64
|
||||
maxConcurrency int
|
||||
}
|
||||
|
||||
// NewBatchWorker 创建批量处理器
|
||||
func NewBatchWorker(maxConcurrency int) *BatchWorker {
|
||||
if maxConcurrency <= 0 {
|
||||
maxConcurrency = 10
|
||||
}
|
||||
return &BatchWorker{
|
||||
maxConcurrency: maxConcurrency,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessConcurrent 并发处理一批数据
|
||||
func (bw *BatchWorker) ProcessConcurrent(
|
||||
ctx context.Context,
|
||||
items []any,
|
||||
handler func(ctx context.Context, item any) (any, error),
|
||||
) BatchResult {
|
||||
bw.total.Add(int64(len(items)))
|
||||
results := BatchResult{}
|
||||
sem := make(chan struct{}, bw.maxConcurrency)
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
for i, item := range items {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
default:
|
||||
}
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(index int, data any) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
bw.running.Add(1)
|
||||
defer bw.running.Add(-1)
|
||||
|
||||
result, err := handler(ctx, data)
|
||||
bw.success.Add(1)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err != nil {
|
||||
bw.failed.Add(1)
|
||||
results.Fail = append(results.Fail, BatchError{
|
||||
Index: index,
|
||||
Item: data,
|
||||
Err: err,
|
||||
})
|
||||
} else {
|
||||
results.Success = append(results.Success, result)
|
||||
}
|
||||
}(i, item)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// ProcessSequential 顺序处理(用于需要严格顺序的场景)
|
||||
func (bw *BatchWorker) ProcessSequential(
|
||||
ctx context.Context,
|
||||
items []any,
|
||||
handler func(ctx context.Context, item any) (any, error),
|
||||
) BatchResult {
|
||||
results := BatchResult{}
|
||||
for i, item := range items {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return results
|
||||
default:
|
||||
}
|
||||
result, err := handler(ctx, item)
|
||||
if err != nil {
|
||||
results.Fail = append(results.Fail, BatchError{Index: i, Item: item, Err: err})
|
||||
} else {
|
||||
results.Success = append(results.Success, result)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Stats 当前处理统计
|
||||
func (bw *BatchWorker) Stats() (running, total, success, failed int64) {
|
||||
return bw.running.Load(), bw.total.Load(), bw.success.Load(), bw.failed.Load()
|
||||
}
|
||||
|
||||
// RingBuffer 无锁环形缓冲区,适合高频消息传递
|
||||
type RingBuffer struct {
|
||||
data []any
|
||||
head int64
|
||||
tail int64
|
||||
capacity int
|
||||
size atomic.Int64
|
||||
}
|
||||
|
||||
// NewRingBuffer 创建环形缓冲区
|
||||
func NewRingBuffer(capacity int) *RingBuffer {
|
||||
if capacity <= 0 {
|
||||
capacity = 1024
|
||||
}
|
||||
rb := &RingBuffer{
|
||||
data: make([]any, capacity),
|
||||
capacity: capacity,
|
||||
}
|
||||
return rb
|
||||
}
|
||||
|
||||
// Enqueue 入队(阻塞直到有空位)
|
||||
func (rb *RingBuffer) Enqueue(item any) {
|
||||
for {
|
||||
tail := atomic.LoadInt64(&rb.tail)
|
||||
head := atomic.LoadInt64(&rb.head)
|
||||
if tail-head >= int64(rb.capacity) {
|
||||
time.Sleep(time.Microsecond)
|
||||
continue
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(&rb.tail, tail, tail+1) {
|
||||
rb.data[tail%int64(rb.capacity)] = item
|
||||
rb.size.Add(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dequeue 出队(阻塞直到有数据)
|
||||
func (rb *RingBuffer) Dequeue() (any, bool) {
|
||||
for {
|
||||
head := atomic.LoadInt64(&rb.head)
|
||||
tail := atomic.LoadInt64(&rb.tail)
|
||||
if head >= tail {
|
||||
return nil, false
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(&rb.head, head, head+1) {
|
||||
item := rb.data[head%int64(rb.capacity)]
|
||||
rb.data[head%int64(rb.capacity)] = nil
|
||||
rb.size.Add(-1)
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size 当前元素数量
|
||||
func (rb *RingBuffer) Size() int64 {
|
||||
return rb.size.Load()
|
||||
}
|
||||
|
||||
// RateLimiter 令牌桶限流器
|
||||
type RateLimiter struct {
|
||||
tokens int64
|
||||
maxTokens int64
|
||||
refillRate int64 // 每秒补充数
|
||||
lastRefill time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewRateLimiter 创建限流器
|
||||
func NewRateLimiter(maxTokens, refillRate int64) *RateLimiter {
|
||||
rl := &RateLimiter{
|
||||
tokens: maxTokens,
|
||||
maxTokens: maxTokens,
|
||||
refillRate: refillRate,
|
||||
lastRefill: time.Now(),
|
||||
}
|
||||
go rl.refillLoop()
|
||||
return rl
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) refillLoop() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
rl.mu.Lock()
|
||||
elapsed := time.Since(rl.lastRefill).Seconds()
|
||||
add := int64(elapsed * float64(rl.refillRate))
|
||||
current := rl.tokens
|
||||
if current+add > rl.maxTokens {
|
||||
add = rl.maxTokens - current
|
||||
}
|
||||
rl.tokens += add
|
||||
rl.lastRefill = time.Now()
|
||||
rl.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire 获取一个令牌,返回是否成功
|
||||
func (rl *RateLimiter) Acquire() bool {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
if rl.tokens <= 0 {
|
||||
return false
|
||||
}
|
||||
rl.tokens--
|
||||
return true
|
||||
}
|
||||
|
||||
// Limit 带限制的包装函数
|
||||
func (rl *RateLimiter) Limit(fn func() error) error {
|
||||
for !rl.Acquire() {
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
}
|
||||
return fn()
|
||||
}
|
||||
|
||||
// ConcurrentMap 线程安全的 map,支持并发读写
|
||||
type ConcurrentMap struct {
|
||||
mu sync.RWMutex
|
||||
items map[any]any
|
||||
}
|
||||
|
||||
// NewConcurrentMap 创建线程安全 map
|
||||
func NewConcurrentMap() *ConcurrentMap {
|
||||
return &ConcurrentMap{items: make(map[any]any)}
|
||||
}
|
||||
|
||||
func (cm *ConcurrentMap) Set(k, v any) {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
cm.items[k] = v
|
||||
}
|
||||
|
||||
func (cm *ConcurrentMap) Get(k any) (any, bool) {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
v, ok := cm.items[k]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (cm *ConcurrentMap) Delete(k any) {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
delete(cm.items, k)
|
||||
}
|
||||
|
||||
func (cm *ConcurrentMap) Len() int {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
return len(cm.items)
|
||||
}
|
||||
|
||||
// Pipeline 函数式管道,链式处理数据流
|
||||
type Pipeline struct {
|
||||
stages []func(any) any
|
||||
input chan any
|
||||
output chan any
|
||||
}
|
||||
|
||||
// NewPipeline 创建管道
|
||||
func NewPipeline() *Pipeline {
|
||||
return &Pipeline{
|
||||
stages: make([]func(any) any, 0),
|
||||
input: make(chan any, 256),
|
||||
output: make(chan any, 256),
|
||||
}
|
||||
}
|
||||
|
||||
// AddStage 添加处理阶段
|
||||
func (p *Pipeline) AddStage(fn func(any) any) *Pipeline {
|
||||
p.stages = append(p.stages, fn)
|
||||
return p
|
||||
}
|
||||
|
||||
// Run 启动管道(阻塞,直到 input 关闭)
|
||||
func (p *Pipeline) Run(items []any) []any {
|
||||
go func() {
|
||||
for _, item := range items {
|
||||
p.input <- item
|
||||
}
|
||||
close(p.input)
|
||||
}()
|
||||
|
||||
go p.process()
|
||||
results := make([]any, 0)
|
||||
for v := range p.output {
|
||||
results = append(results, v)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (p *Pipeline) process() {
|
||||
defer close(p.output)
|
||||
for item := range p.input {
|
||||
for _, stage := range p.stages {
|
||||
item = stage(item)
|
||||
}
|
||||
p.output <- item
|
||||
}
|
||||
}
|
||||
|
||||
// GoCtx 便捷并发模式
|
||||
type GoCtx struct {
|
||||
wg sync.WaitGroup
|
||||
errs sync.Map
|
||||
results sync.Map
|
||||
}
|
||||
|
||||
// Go 启动一个并发任务
|
||||
func (gc *GoCtx) Go(id string, fn func() error) {
|
||||
gc.wg.Add(1)
|
||||
go func() {
|
||||
defer gc.wg.Done()
|
||||
if err := fn(); err != nil {
|
||||
gc.errs.Store(id, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait 等待所有任务完成,返回错误列表
|
||||
func (gc *GoCtx) Wait() []error {
|
||||
gc.wg.Wait()
|
||||
var errs []error
|
||||
gc.errs.Range(func(_, v any) bool {
|
||||
errs = append(errs, v.(error))
|
||||
return true
|
||||
})
|
||||
return errs
|
||||
}
|
||||
|
||||
// WaitWithTimeout 带超时的等待
|
||||
func (gc *GoCtx) WaitWithTimeout(timeout time.Duration) []error {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
gc.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
var errs []error
|
||||
gc.errs.Range(func(_, v any) bool {
|
||||
errs = append(errs, v.(error))
|
||||
return true
|
||||
})
|
||||
return errs
|
||||
case <-time.After(timeout):
|
||||
return []error{fmt.Errorf("并发任务超时: %v", timeout)}
|
||||
}
|
||||
}
|
||||
167
internal/tray/win/tray.go
Normal file
167
internal/tray/win/tray.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// Package tray Windows 系统托盘实现(使用 win32 API)
|
||||
package tray
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// Hook 托盘图标句柄
|
||||
var hook windows.Handle
|
||||
|
||||
// MenuHandle 菜单句柄
|
||||
type MenuHandle uintptr
|
||||
|
||||
// Tray 系统托盘管理器
|
||||
type Tray struct {
|
||||
mu sync.Mutex
|
||||
visible bool
|
||||
callback uintptr // WNDPROC 回调
|
||||
hWnd windows.Handle
|
||||
}
|
||||
|
||||
// MenuItem 菜单项
|
||||
type MenuItem struct {
|
||||
ID uint32
|
||||
Text string
|
||||
}
|
||||
|
||||
// NewTray 创建系统托盘
|
||||
func NewTray(iconData []byte, tooltip string) *Tray {
|
||||
t := &Tray{}
|
||||
if err := t.init(iconData, tooltip); err != nil {
|
||||
log.Printf("[tray] init failed: %v", err)
|
||||
return nil
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Tray) init(iconData []byte, tooltip string) error {
|
||||
// 获取当前进程窗口
|
||||
hWnd, _ := windows.GetForegroundWindow()
|
||||
if hWnd == 0 {
|
||||
return windows.ERROR_ACCESS_DENIED
|
||||
}
|
||||
t.hWnd = hWnd
|
||||
|
||||
// 注册托盘图标
|
||||
// NOTIFYICONDATA structure
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa
|
||||
data := ¬ifyIconData{
|
||||
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||
HWnd: uint32(hWnd),
|
||||
UID: 1000,
|
||||
UFlags: NIF_MESSAGE | NIF_ICON | NIF_TIP,
|
||||
UCallbackMessage: WM_USER + 1000,
|
||||
HIcon: 0, // 使用默认图标
|
||||
}
|
||||
|
||||
// 设置 tooltip
|
||||
for i := 0; i < len(tooltip) && i < 128; i++ {
|
||||
data.Tip[i] = uint8(tooltip[i])
|
||||
}
|
||||
data.Tip[len(tooltip)] = 0
|
||||
|
||||
// 添加托盘图标
|
||||
ret, _, _ := procShellNotifyIcon.Call(
|
||||
SHM_ADD,
|
||||
uintptr(unsafe.Pointer(data)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return windows.GetLastError()
|
||||
}
|
||||
|
||||
t.visible = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy 销毁托盘图标
|
||||
func (t *Tray) Destroy() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if !t.visible {
|
||||
return
|
||||
}
|
||||
data := ¬ifyIconData{
|
||||
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||
HWnd: uint32(t.hWnd),
|
||||
UID: 1000,
|
||||
UFlags: NIF_MESSAGE,
|
||||
}
|
||||
procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
|
||||
t.visible = false
|
||||
}
|
||||
|
||||
// Show 显示托盘
|
||||
func (t *Tray) Show(iconData []byte, tooltip string) error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.visible {
|
||||
return nil
|
||||
}
|
||||
return t.init(iconData, tooltip)
|
||||
}
|
||||
|
||||
// Hide 隐藏托盘
|
||||
func (t *Tray) Hide() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if !t.visible {
|
||||
return
|
||||
}
|
||||
data := ¬ifyIconData{
|
||||
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||
HWnd: uint32(t.hWnd),
|
||||
UID: 1000,
|
||||
UFlags: NIF_MESSAGE,
|
||||
}
|
||||
procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
|
||||
t.visible = false
|
||||
}
|
||||
|
||||
// IsVisible 是否可见
|
||||
func (t *Tray) IsVisible() bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return t.visible
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Win32 API 定义
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
WM_USER = 0x0400
|
||||
SHM_ADD uintptr = 0x00000000
|
||||
SHM_DELETE uintptr = 0x00000001
|
||||
|
||||
NIF_MESSAGE = 0x00000001
|
||||
NIF_ICON = 0x00000002
|
||||
NIF_TIP = 0x00000004
|
||||
)
|
||||
|
||||
// NOTIFYICONDATA struct
|
||||
type notifyIconData struct {
|
||||
CbSize uint32
|
||||
HWnd uint32
|
||||
UID uint32
|
||||
UFlags uint32
|
||||
UCallbackMessage uint32
|
||||
HIcon uintptr
|
||||
Tip [128]uint8
|
||||
}
|
||||
|
||||
var (
|
||||
procShellNotifyIcon = mustLoad("shell32.dll").MustFindProc("Shell_NotifyIconA")
|
||||
)
|
||||
|
||||
func mustLoad(dllName string) *windows.DynLib {
|
||||
dll, err := windows.LoadDLL(dllName)
|
||||
if err != nil {
|
||||
log.Fatalf("[tray] failed to load %s: %v", dllName, err)
|
||||
}
|
||||
return dll
|
||||
}
|
||||
146
internal/worker/pool.go
Normal file
146
internal/worker/pool.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Job 任务接口,所有后台任务需实现此接口
|
||||
type Job interface {
|
||||
Execute(ctx context.Context) error
|
||||
JobID() string
|
||||
}
|
||||
|
||||
// TaskResult 任务执行结果
|
||||
type TaskResult struct {
|
||||
JobID string
|
||||
Output any
|
||||
Error error
|
||||
}
|
||||
|
||||
// Pool 任务并发池,管理固定数量工作协程
|
||||
type Pool struct {
|
||||
jobs chan Job
|
||||
results chan TaskResult
|
||||
wg sync.WaitGroup
|
||||
stopOnce sync.Once
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
running atomic.Int64
|
||||
total atomic.Int64
|
||||
failed atomic.Int64
|
||||
}
|
||||
|
||||
// Options 池配置
|
||||
type Options struct {
|
||||
Workers int // 工作协程数,默认 4
|
||||
BufSize int // 队列缓冲,默认 1000
|
||||
}
|
||||
|
||||
// NewPool 创建任务池
|
||||
func NewPool(opts ...Options) *Pool {
|
||||
o := Options{Workers: 4, BufSize: 1000}
|
||||
if len(opts) > 0 {
|
||||
o = opts[0]
|
||||
}
|
||||
if o.Workers <= 0 {
|
||||
o.Workers = 4
|
||||
}
|
||||
if o.BufSize <= 0 {
|
||||
o.BufSize = 1000
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p := &Pool{
|
||||
jobs: make(chan Job, o.BufSize),
|
||||
results: make(chan TaskResult, o.BufSize),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
p.startWorkers()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Pool) startWorkers() {
|
||||
for i := 0; i < 4; i++ {
|
||||
p.wg.Add(1)
|
||||
go p.worker()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) worker() {
|
||||
defer p.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
case job, ok := <-p.jobs:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.running.Add(1)
|
||||
p.total.Add(1)
|
||||
err := job.Execute(p.ctx)
|
||||
result := TaskResult{JobID: job.JobID(), Error: err}
|
||||
if err != nil {
|
||||
p.failed.Add(1)
|
||||
}
|
||||
p.running.Add(-1)
|
||||
select {
|
||||
case p.results <- result:
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit 提交任务到队列(非阻塞,队列满时返回 false)
|
||||
func (p *Pool) Submit(job Job) bool {
|
||||
select {
|
||||
case p.jobs <- job:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitAndWait 提交任务并等待结果
|
||||
func (p *Pool) SubmitAndWait(ctx context.Context, job Job) (*TaskResult, error) {
|
||||
if !p.Submit(job) {
|
||||
return nil, ErrQueueFull
|
||||
}
|
||||
select {
|
||||
case r := <-p.results:
|
||||
return &r, r.Error
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Results 返回结果通道,供外部 range 消费
|
||||
func (p *Pool) Results() <-chan TaskResult {
|
||||
return p.results
|
||||
}
|
||||
|
||||
// Stats 当前池状态
|
||||
func (p *Pool) Stats() (running, total, failed int64) {
|
||||
return p.running.Load(), p.total.Load(), p.failed.Load()
|
||||
}
|
||||
|
||||
// Stop 优雅停止池
|
||||
func (p *Pool) Stop() {
|
||||
p.stopOnce.Do(func() {
|
||||
p.cancel()
|
||||
close(p.jobs)
|
||||
p.wg.Wait()
|
||||
close(p.results)
|
||||
})
|
||||
}
|
||||
|
||||
var ErrQueueFull = SyncError("任务队列已满,请降低提交频率或增大 BufSize")
|
||||
|
||||
// SyncError 用于类型断言
|
||||
type SyncError string
|
||||
|
||||
func (e SyncError) Error() string { return string(e) }
|
||||
50
main.go
Normal file
50
main.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"log"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
|
||||
"go_win_template/internal/plugins"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
//go:embed assets/icon.png
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
wailsApp := NewApp()
|
||||
|
||||
cfg := &options.App{
|
||||
Title: "go_win_template",
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
MinWidth: 1024,
|
||||
MinHeight: 600,
|
||||
WindowStartState: options.Normal,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: func(ctx context.Context) {
|
||||
log.Println("[main] startup complete")
|
||||
// 启动插件
|
||||
if err := plugins.Start(ctx); err != nil {
|
||||
log.Printf("[main] plugin start error: %v", err)
|
||||
}
|
||||
plugins.PrintStatus()
|
||||
},
|
||||
Bind: []interface{}{
|
||||
wailsApp,
|
||||
},
|
||||
}
|
||||
|
||||
err := wails.Run(cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
BIN
trayicons/icon.png
Normal file
BIN
trayicons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 477 B |
18
wails.json
Normal file
18
wails.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "go_win_template",
|
||||
"outputfilename": "go_win_template",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "Geliebte",
|
||||
"email": "1297754537@qq.com"
|
||||
},
|
||||
"info": {
|
||||
"productname": "go_win_template",
|
||||
"version": "1.0.0",
|
||||
"copyright": ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user