• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

b1f6c1c4 / ProfessionalAccounting / 327

28 Apr 2025 05:37PM UTC coverage: 51.346% (-3.0%) from 54.325%
327

push

appveyor

b1f6c1c4
ci buildx

6620 of 12893 relevant lines covered (51.35%)

126.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.91
/AccountingServer.Shell/AccountingShell.cs
1
/* Copyright (C) 2020-2025 b1f6c1c4
2
 *
3
 * This file is part of ProfessionalAccounting.
4
 *
5
 * ProfessionalAccounting is free software: you can redistribute it and/or
6
 * modify it under the terms of the GNU Affero General Public License as
7
 * published by the Free Software Foundation, version 3.
8
 *
9
 * ProfessionalAccounting is distributed in the hope that it will be useful, but
10
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
12
 * for more details.
13
 *
14
 * You should have received a copy of the GNU Affero General Public License
15
 * along with ProfessionalAccounting.  If not, see
16
 * <https://www.gnu.org/licenses/>.
17
 */
18

19
using System;
20
using System.Collections.Generic;
21
using System.Linq;
22
using System.Security;
23
using AccountingServer.BLL;
24
using AccountingServer.BLL.Util;
25
using AccountingServer.Entities;
26
using AccountingServer.Entities.Util;
27
using AccountingServer.Shell.Subtotal;
28
using AccountingServer.Shell.Util;
29
using static AccountingServer.BLL.Parsing.FacadeF;
30
using static AccountingServer.BLL.Parsing.Synthesizer;
31

32
namespace AccountingServer.Shell;
33

34
/// <summary>
35
///     基本表达式解释器
36
/// </summary>
37
internal class AccountingShell : IShellComponent
38
{
39
    /// <inheritdoc />
40
    public IAsyncEnumerable<string> Execute(string expr, Context ctx, string term)
41
        => Parse(expr, ctx.Client)(ctx);
6✔
42

43
    /// <inheritdoc />
44
    public bool IsExecutable(string expr) => true;
6✔
45

46
    /// <summary>
47
    ///     解析表达式
48
    /// </summary>
49
    /// <param name="expr">表达式</param>
50
    /// <param name="client">客户端</param>
51
    /// <returns>解析结果</returns>
52
    private Func<Context, IAsyncEnumerable<string>> Parse(string expr, Client client)
53
    {
6✔
54
        var type = ExprType.None;
6✔
55
        ISubtotalStringify visitor;
56

57
        var explain = ParsingF.Token(ref expr, false, static t => t == "explain") != null;
6✔
58

59
        if (ParsingF.Token(ref expr, false, static t => t == "unsafe") != null)
6✔
60
            type |= ExprType.Unsafe;
2✔
61

62
        if (ParsingF.Token(ref expr, false, static t => t == "json") != null)
6✔
63
        {
1✔
64
            type |= ExprType.GroupedQueries;
1✔
65
            visitor = new JsonSubtotal();
1✔
66
        }
1✔
67
        else if (ParsingF.Token(ref expr, false, static t => t == "Rps") != null)
5✔
68
        {
×
69
            type |= ExprType.GroupedQueries;
×
70
            visitor = new PreciseSubtotalPre();
×
71
        }
×
72
        else if (ParsingF.Token(ref expr, false, static t => t == "rps") != null)
5✔
73
        {
×
74
            type |= ExprType.GroupedQueries;
×
75
            visitor = new PreciseSubtotalPre(false);
×
76
        }
×
77
        else if (ParsingF.Token(ref expr, false, static t => t == "raw") != null)
5✔
78
        {
×
79
            type |= ExprType.GroupedQueries | ExprType.DetailQuery;
×
80
            visitor = new RawSubtotal(0);
×
81
        }
×
82
        else if (ParsingF.Token(ref expr, false, static t => t == "sraw") != null)
5✔
83
        {
2✔
84
            type |= ExprType.GroupedQueries | ExprType.DetailRQuery;
2✔
85
            visitor = new RawSubtotal(0);
2✔
86
        }
2✔
87
        else if (ParsingF.Token(ref expr, false, static t => t == "fancy") != null)
3✔
88
        {
1✔
89
            type |= ExprType.FancyQuery;
1✔
90
            visitor = null;
1✔
91
        }
1✔
92
        else if (ParsingF.Token(ref expr, false, static t => t.StartsWith("raw", StringComparison.OrdinalIgnoreCase)) is var t && t != null)
2✔
93
        {
×
94
            type |= ExprType.GroupedQueries;
×
95
            visitor = new RawSubtotal(int.Parse(t[4..]))
×
96
                {
97
                    Ratio = t[3] switch
×
98
                        {
99
                            '+' => +1.0,
×
100
                            '-' => -1.0,
×
101
                            _ => throw new ApplicationException("Unknown raw ratio specifier."),
×
102
                        },
103
                    Inject = ParsingF.Quoted(ref expr, '@'),
104
                };
105
        }
×
106
        else
107
        {
2✔
108
            type |= ExprType.GroupedQueries | ExprType.VoucherQuery;
2✔
109
            visitor = new RichSubtotalPre(ParsingF.Token(ref expr, false, static t => t == "force") != null);
2✔
110
        }
2✔
111

112
        (visitor as IClientDependable).Assign(client);
6✔
113

114
        if (type.HasFlag(ExprType.GroupedQuery))
6✔
115
            try
116
            {
5✔
117
                return TryGroupedQuery(expr, visitor, client, explain);
5✔
118
            }
119
            catch (Exception)
4✔
120
            {
4✔
121
                // ignored
122
            }
4✔
123

124
        if (type.HasFlag(ExprType.VoucherGroupedQuery))
5✔
125
            try
126
            {
4✔
127
                return TryVoucherGroupedQuery(expr, visitor, client, explain);
4✔
128
            }
129
            catch (Exception)
4✔
130
            {
4✔
131
                // ignored
132
            }
4✔
133

134
        return (type & ExprType.NonGroupedQueries) switch
×
135
            {
136
                ExprType.VoucherQuery => TryVoucherQuery(expr, !type.HasFlag(ExprType.Unsafe), client, explain),
2✔
137
                ExprType.DetailQuery => TryDetailQuery(expr, !type.HasFlag(ExprType.Unsafe), client, explain),
×
138
                ExprType.DetailRQuery => TryDetailRQuery(expr, !type.HasFlag(ExprType.Unsafe), client, explain),
2✔
139
                ExprType.FancyQuery => TryFancyQuery(expr, !type.HasFlag(ExprType.Unsafe), client, explain),
1✔
140
                _ => throw new InvalidOperationException("表达式无效"),
×
141
            };
142
    }
4✔
143

144
    private async IAsyncEnumerable<string> Present(params string[] ss)
145
    {
×
146
        foreach (var s in ss)
×
147
            yield return $"{s}\n";
×
148
    }
×
149

150
    /// <summary>
151
    ///     按记账凭证检索式解析
152
    /// </summary>
153
    /// <param name="expr">表达式</param>
154
    /// <param name="safe">仅限强检索式</param>
155
    /// <param name="client">客户端</param>
156
    /// <returns>执行结果</returns>
157
    private Func<Context, IAsyncEnumerable<string>> TryVoucherQuery(string expr, bool safe, Client client, bool explain)
158
    {
2✔
159
        var res = ParsingF.VoucherQuery(ref expr, client);
2✔
160
        ParsingF.Eof(expr);
2✔
161
        if (res.IsDangerous() && safe && !explain)
1✔
162
            throw new SecurityException("检测到弱检索式");
×
163

164
        return ctx => explain
1✔
165
            ? Present("IQueryCompounded<IVoucherQueryAtom>", Synth(res), Synth(ctx.Identity.Refine(res, true)))
166
            : PresentVoucherQuery(ctx.Identity.Refine(res), ctx);
167
    }
1✔
168

169
    /// <summary>
170
    ///     按分类汇总检索式解析
171
    /// </summary>
172
    /// <param name="expr">表达式</param>
173
    /// <param name="trav">呈现器</param>
174
    /// <param name="client">客户端</param>
175
    /// <returns>执行结果</returns>
176
    private Func<Context, IAsyncEnumerable<string>> TryGroupedQuery(string expr, ISubtotalStringify trav, Client client,
177
            bool explain)
178
    {
5✔
179
        var res = ParsingF.GroupedQuery(ref expr, client);
5✔
180
        ParsingF.Eof(expr);
1✔
181
        return ctx => explain
1✔
182
            ? Present("IGroupedQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
183
            : PresentSubtotal(ctx.Identity.Refine(res), trav, ctx);
184
    }
1✔
185

186
    /// <summary>
187
    ///     按记账凭证分类汇总检索式解析
188
    /// </summary>
189
    /// <param name="expr">表达式</param>
190
    /// <param name="trav">呈现器</param>
191
    /// <param name="client">客户端</param>
192
    /// <returns>执行结果</returns>
193
    private Func<Context, IAsyncEnumerable<string>> TryVoucherGroupedQuery(string expr, ISubtotalStringify trav,
194
        Client client, bool explain)
195
    {
4✔
196
        var res = ParsingF.VoucherGroupedQuery(ref expr, client);
4✔
197
        ParsingF.Eof(expr);
×
198
        return ctx => explain
×
199
            ? Present("IVoucherGroupedQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
200
            : PresentSubtotal(ctx.Identity.Refine(res), trav, ctx);
201
    }
×
202

203
    /// <summary>
204
    ///     执行记账凭证检索式并呈现记账凭证
205
    /// </summary>
206
    /// <param name="query">记账凭证检索式</param>
207
    /// <param name="ctx">客户端上下文</param>
208
    /// <returns>记账凭证表达式</returns>
209
    private IAsyncEnumerable<string> PresentVoucherQuery(IQueryCompounded<IVoucherQueryAtom> query, Context ctx)
210
        => ctx.Serializer.PresentVouchers(ctx.Accountant.SelectVouchersAsync(query)
1✔
211
                .Select(ctx.Identity.RedactDetails));
212

213
    /// <summary>
214
    ///     按细目检索式解析
215
    /// </summary>
216
    /// <param name="expr">表达式</param>
217
    /// <param name="safe">仅限强检索式</param>
218
    /// <param name="client">客户端</param>
219
    /// <returns>执行结果</returns>
220
    private Func<Context, IAsyncEnumerable<string>> TryDetailQuery(string expr, bool safe, Client client, bool explain)
221
    {
×
222
        var res = ParsingF.DetailQuery(ref expr, client);
×
223
        ParsingF.Eof(expr);
×
224
        if (res.IsDangerous() && safe && !explain)
×
225
            throw new SecurityException("检测到弱检索式");
×
226

227
        return ctx => explain
×
228
            ? Present("IVoucherDetailQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
229
            : PresentDetailQuery(ctx.Identity.Refine(res), ctx);
230
    }
×
231

232
    /// <summary>
233
    ///     执行细目检索式并呈现结果
234
    /// </summary>
235
    /// <param name="query">细目检索式</param>
236
    /// <param name="ctx">客户端上下文</param>
237
    /// <returns>执行结果</returns>
238
    private IAsyncEnumerable<string> PresentDetailQuery(IVoucherDetailQuery query, Context ctx)
239
        => ctx.Serializer.PresentVoucherDetails(ctx.Accountant.SelectVoucherDetailsAsync(query));
×
240

241
    /// <summary>
242
    ///     按带记账凭证的细目检索式解析
243
    /// </summary>
244
    /// <param name="expr">表达式</param>
245
    /// <param name="safe">仅限强检索式</param>
246
    /// <param name="client">客户端</param>
247
    /// <returns>执行结果</returns>
248
    private Func<Context, IAsyncEnumerable<string>> TryDetailRQuery(string expr, bool safe, Client client, bool explain)
249
    {
2✔
250
        var res = ParsingF.DetailQuery(ref expr, client);
2✔
251
        ParsingF.Eof(expr);
2✔
252
        if (res.IsDangerous() && safe && !explain)
2✔
253
            throw new SecurityException("检测到弱检索式");
1✔
254

255
        return ctx => explain
1✔
256
            ? Present("IVoucherDetailQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
257
            : PresentDetailRQuery(ctx.Identity.Refine(res), ctx);
258
    }
1✔
259

260
    /// <summary>
261
    ///     执行带记账凭证的细目检索式并呈现结果
262
    /// </summary>
263
    /// <param name="query">细目检索式</param>
264
    /// <param name="ctx">客户端上下文</param>
265
    /// <returns>执行结果</returns>
266
    private IAsyncEnumerable<string> PresentDetailRQuery(IVoucherDetailQuery query, Context ctx)
267
        => ctx.Serializer.PresentVoucherDetails(
1✔
268
            ctx.Accountant.SelectVouchersAsync(query?.VoucherQuery).SelectMany(
269
                v => v.Details.Where(d => d.IsMatch(query?.ActualDetailFilter()))
4✔
270
                    .Select(d => new VoucherDetailR(v, d)).ToAsyncEnumerable()));
2✔
271

272
    /// <summary>
273
    ///     按记账凭证检索式解析,但仅保留部分细目
274
    /// </summary>
275
    /// <param name="expr">表达式</param>
276
    /// <param name="safe">仅限强检索式</param>
277
    /// <param name="client">客户端</param>
278
    /// <returns>执行结果</returns>
279
    private Func<Context, IAsyncEnumerable<string>> TryFancyQuery(string expr, bool safe, Client client, bool explain)
280
    {
1✔
281
        var res = ParsingF.DetailQuery(ref expr, client);
1✔
282
        ParsingF.Eof(expr);
1✔
283
        if (res.IsDangerous() && safe && !explain)
1✔
284
            throw new SecurityException("检测到弱检索式");
×
285

286
        return ctx => explain
1✔
287
            ? Present("IVoucherDetailQuery", Synth(res), Synth(ctx.Identity.Refine(res, true)))
288
            : PresentFancyQuery(ctx.Identity.Refine(res), ctx);
289
    }
1✔
290

291
    /// <summary>
292
    ///     执行记账凭证检索式,保留部分细目并呈现结果
293
    /// </summary>
294
    /// <param name="query">带记账凭证的细目检索式</param>
295
    /// <param name="ctx">客户端上下文</param>
296
    /// <returns>执行结果</returns>
297
    private IAsyncEnumerable<string> PresentFancyQuery(IVoucherDetailQuery query, Context ctx)
298
        => ctx.Serializer.PresentVouchers(
1✔
299
            ctx.Accountant.SelectVouchersAsync(query?.VoucherQuery).Select(v =>
300
                {
1✔
301
                    v.Details.RemoveAll(d => !d.IsMatch(query?.ActualDetailFilter()));
4✔
302
                    return v;
1✔
303
                }));
1✔
304

305
    /// <summary>
306
    ///     执行记账凭证分类汇总检索式并呈现结果
307
    /// </summary>
308
    /// <param name="query">记账凭证分类汇总检索式</param>
309
    /// <param name="trav">呈现器</param>
310
    /// <param name="ctx">客户端上下文</param>
311
    /// <returns>执行结果</returns>
312
    private async IAsyncEnumerable<string> PresentSubtotal(IVoucherGroupedQuery query, ISubtotalStringify trav,
313
        Context ctx)
314
    {
×
315
        var result = await ctx.Accountant.SelectVouchersGroupedAsync(query);
×
316
        await foreach (var s in trav.PresentSubtotal(result, query.Subtotal, ctx.Serializer))
×
317
            yield return s;
×
318
    }
×
319

320
    /// <summary>
321
    ///     执行分类汇总检索式并呈现结果
322
    /// </summary>
323
    /// <param name="query">分类汇总检索式</param>
324
    /// <param name="trav">呈现器</param>
325
    /// <param name="ctx">客户端上下文</param>
326
    /// <returns>执行结果</returns>
327
    private async IAsyncEnumerable<string> PresentSubtotal(IGroupedQuery query, ISubtotalStringify trav,
328
        Context ctx)
329
    {
1✔
330
        var result = await ctx.Accountant.SelectVoucherDetailsGroupedAsync(query);
1✔
331
        await foreach (var s in trav.PresentSubtotal(result, query.Subtotal, ctx.Serializer))
2✔
332
            yield return s;
1✔
333
    }
1✔
334

335
    [Flags]
336
    private enum ExprType
337
    {
338
        None = 0b00000000,
339
        GroupedQueries = 0b00000011,
340
        GroupedQuery = 0b00000001,
341
        VoucherGroupedQuery = 0b00000010,
342
        NonGroupedQueries = 0b00111100,
343
        DetailQuery = 0b00000100,
344
        DetailRQuery = 0b00001000,
345
        VoucherQuery = 0b00010000,
346
        FancyQuery = 0b00100000,
347
        Unsafe = 0b10000000,
348
    }
349
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc