diff --git a/modules/EcommerceFinance/Http/Controllers/AccountBills.php b/modules/EcommerceFinance/Http/Controllers/AccountBills.php index 154b470a..4fe2a186 100644 --- a/modules/EcommerceFinance/Http/Controllers/AccountBills.php +++ b/modules/EcommerceFinance/Http/Controllers/AccountBills.php @@ -136,6 +136,29 @@ class AccountBills extends Controller return $this->exportQuery($request, $query, $bill_type); } + public function filterOptions(string $bill_type) + { + $this->authorizeStoreReceivingBills(); + + abort_unless($bill_type === 'douyin', 404, '该账户类型没有可用的筛选选项。'); + + try { + return response()->json( + app(StoreReceivingDouyinBillMirrorRepository::class)->filterOptions() + ); + } catch (RuntimeException $exception) { + if ($exception->getMessage() !== 'store_receiving_alipay_bill_mirror_not_ready') { + throw $exception; + } + + return response()->json([ + 'message' => '抖音账户账单共享业务镜像正在首次同步,筛选选项稍后可用。', + 'shops' => [], + 'bill_items' => [], + ], 503); + } + } + private function exportQuery(Request $request, $query, string $billType) { $definition = AccountBillColumns::for($billType); diff --git a/modules/EcommerceFinance/Repositories/StoreReceivingAlipayBillMirrorRepository.php b/modules/EcommerceFinance/Repositories/StoreReceivingAlipayBillMirrorRepository.php index cf27c36a..a7659c0d 100644 --- a/modules/EcommerceFinance/Repositories/StoreReceivingAlipayBillMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/StoreReceivingAlipayBillMirrorRepository.php @@ -138,6 +138,54 @@ class StoreReceivingAlipayBillMirrorRepository return $this->exportBatches($query, $sortColumn, $direction, $shopNames, $mapping); } + /** @return array{shops:array,bill_items:array} */ + public function filterOptionsForChannel(string $channel): array + { + $dataset = $this->datasets->get(self::DATASET); + if (! $this->mirror->ready($dataset) + || ! $this->mirror->ready($this->datasets->get(self::SHOP_DATASET))) { + throw new RuntimeException('store_receiving_alipay_bill_mirror_not_ready'); + } + + $channelQuery = $this->billQuery(); + $this->applyChannel($channelQuery, $channel); + + $shopIds = (clone $channelQuery) + ->whereNotNull('b.shop_id') + ->distinct() + ->pluck('b.shop_id') + ->filter(static fn (mixed $value): bool => $value !== null && $value !== '') + ->all(); + + $shops = $shopIds === [] + ? collect() + : $this->mirror->connection()->table('ops_jst_shop_info') + ->whereIn('shop_id', $shopIds) + ->whereNotNull('shop_name') + ->orderBy('shop_name') + ->pluck('shop_name'); + + $billItems = (clone $channelQuery) + ->whereNotNull('b.fee_name') + ->where('b.fee_name', '<>', '') + ->select('b.fee_name') + ->distinct() + ->orderBy('b.fee_name') + ->pluck('b.fee_name'); + + $normalize = static fn ($values): array => collect($values) + ->map(static fn (mixed $value): string => trim((string) $value)) + ->filter() + ->unique() + ->values() + ->all(); + + return [ + 'shops' => $normalize($shops), + 'bill_items' => $normalize($billItems), + ]; + } + /** Read bounded pages so a full export does not sort/buffer millions of rows at once. */ private function exportBatches(Builder $query, string $sortColumn, string $direction, array $shopNames, string $mapping): iterable { diff --git a/modules/EcommerceFinance/Repositories/StoreReceivingDouyinBillMirrorRepository.php b/modules/EcommerceFinance/Repositories/StoreReceivingDouyinBillMirrorRepository.php index fa6092a9..4a605852 100644 --- a/modules/EcommerceFinance/Repositories/StoreReceivingDouyinBillMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/StoreReceivingDouyinBillMirrorRepository.php @@ -37,4 +37,10 @@ class StoreReceivingDouyinBillMirrorRepository 'douyin', ); } + + /** @return array{shops:array,bill_items:array} */ + public function filterOptions(): array + { + return $this->source->filterOptionsForChannel(self::SOURCE_CHANNEL); + } } diff --git a/resources/assets/js/components/business/AccountBills.vue b/resources/assets/js/components/business/AccountBills.vue index ffe967f8..48365b53 100644 --- a/resources/assets/js/components/business/AccountBills.vue +++ b/resources/assets/js/components/business/AccountBills.vue @@ -4,11 +4,24 @@
{{ stat.label }}
{{ stat.value }}
{{ stat.note }}
- - - + + + -
+
+
@@ -43,18 +56,21 @@ import TableColumnSettings from './TableColumnSettings.vue'; const SORTABLE = ['occurred_at','income','expense','balance','record_code']; export default { name:'AccountBills', components:{ TableColumnSettings }, props:{ apiBase:{type:String,required:true}, billType:{type:String,required:true}, userKey:{type:[String,Number],required:true} }, - data(){return{loading:false,loadError:false,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:20,filters:{store_name:'',bill_item:'',start:'',end:'',keyword:''},sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible'};}, + data(){return{loading:false,loadError:false,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:20,filters:{store_name:'',bill_item:'',start:'',end:'',keyword:''},filterOptions:{shops:[],bill_items:[]},filterOptionsLoading:false,filterOptionsError:false,requestSerial:0,sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible'};}, computed:{ + isDouyin(){return this.billType==='douyin';}, + invalidDateRange(){return Boolean(this.filters.start&&this.filters.end&&this.filters.start>this.filters.end);}, visibleColumns(){return this.configuredColumns.filter(column=>column.visible);}, visibleGroups(){return this.visibleColumns.reduce((groups,column)=>{if(!column.group)return groups;const last=groups[groups.length-1];if(last&&last.label===column.group)last.span+=1;else groups.push({label:column.group,span:1});return groups;},[]);}, stats(){return[{label:'收入合计',value:this.money(this.summary.income),note:`${this.summary.count} 笔交易`,className:'is-income'},{label:'支出合计',value:this.money(this.summary.expense),note:'按当前筛选',className:'is-expense'},{label:'期末余额(最新)',value:this.money(this.summary.balance),note:'最新一条记录',className:''},{label:'交易笔数',value:this.summary.count,note:'当前筛选结果',className:''}];}, rangeStart(){return this.total===0?0:(this.currentPage-1)*this.perPage+1;},rangeEnd(){return Math.min(this.currentPage*this.perPage,this.total);} }, - mounted(){this.fetch();}, + mounted(){if(this.isDouyin)this.fetchFilterOptions();this.fetch();}, methods:{ - async fetch(){this.loading=true;this.loadError=false;try{const response=await window.axios.get(this.apiBase,{params:{...this.filters,sort:this.sort.key,dir:this.sort.dir,limit:this.perPage,page:this.currentPage}});const body=response.data||{};this.rows=body.data||[];this.total=body.total||0;this.currentPage=body.current_page||1;this.lastPage=body.last_page||1;this.perPage=body.per_page||this.perPage;this.summary=body.summary||{income:0,expense:0,balance:0,count:0};if(body.columns){this.columnsTitle=body.columns.title||'';this.sourceGroups=body.columns.groups||[];this.sourceColumns=this.attachGroups(body.columns.columns||body.columns||[],this.sourceGroups);}}catch(error){this.rows=[];this.total=0;this.summary={income:0,expense:0,balance:0,count:0};this.loadError=true;}finally{this.loading=false;}}, + async fetchFilterOptions(){this.filterOptionsLoading=true;this.filterOptionsError=false;try{const response=await window.axios.get(`${this.apiBase}/filter-options`);const body=response.data||{};this.filterOptions={shops:Array.isArray(body.shops)?body.shops:[],bill_items:Array.isArray(body.bill_items)?body.bill_items:[]};}catch(error){this.filterOptionsError=true;}finally{this.filterOptionsLoading=false;}}, + async fetch(){const requestId=++this.requestSerial;this.loading=true;this.loadError=false;try{const response=await window.axios.get(this.apiBase,{params:{...this.filters,sort:this.sort.key,dir:this.sort.dir,limit:this.perPage,page:this.currentPage}});if(requestId!==this.requestSerial)return;const body=response.data||{};this.rows=body.data||[];this.total=body.total||0;this.currentPage=body.current_page||1;this.lastPage=body.last_page||1;this.perPage=body.per_page||this.perPage;this.summary=body.summary||{income:0,expense:0,balance:0,count:0};if(body.columns){this.columnsTitle=body.columns.title||'';this.sourceGroups=body.columns.groups||[];this.sourceColumns=this.attachGroups(body.columns.columns||body.columns||[],this.sourceGroups);}}catch(error){if(requestId!==this.requestSerial)return;this.rows=[];this.total=0;this.summary={income:0,expense:0,balance:0,count:0};this.loadError=true;}finally{if(requestId===this.requestSerial)this.loading=false;}}, attachGroups(columns,groups){let index=0;return columns.map((column,columnIndex)=>{while(groups[index]&&columnIndex>=groups.slice(0,index+1).reduce((sum,group)=>sum+Number(group.span||0),0))index+=1;return{...column,key:column.field||`column-${columnIndex}`,group:groups[index]?groups[index].label:''};});}, - applyFilters(){this.currentPage=1;this.fetch();},resetFilters(){this.filters={store_name:'',bill_item:'',start:'',end:'',keyword:''};this.applyFilters();},changePage(page){this.currentPage=page;this.fetch();this.$nextTick(()=>{if(this.$refs.scroll)this.$refs.scroll.scrollTop=0;});}, + applyFilters(){if(this.invalidDateRange)return;this.currentPage=1;this.fetch();},resetFilters(){this.filters={store_name:'',bill_item:'',start:'',end:'',keyword:''};this.applyFilters();},changePage(page){this.currentPage=page;this.fetch();this.$nextTick(()=>{if(this.$refs.scroll)this.$refs.scroll.scrollTop=0;});}, maybeSort(column){if(!this.sortableField(column.field))return;this.sort.dir=this.sort.key===column.field&&this.sort.dir==='desc'?'asc':'desc';this.sort.key=column.field;this.applyFilters();},sortableField(field){return SORTABLE.indexOf(field)!==-1;},sortMark(field){return this.sort.key===field?(this.sort.dir==='asc'?'↑':'↓'):'↕';}, getValue(row,field){if(!field)return'';if(field.indexOf('payload.')===0){const payload=row.payload||{};const value=payload[field.slice(8)];return value===null||value===undefined?'':value;}const value=row[field];return value===null||value===undefined?'':value;},rawValue(row,column){return this.getValue(row,column.field);}, displayValue(row,column){const value=this.rawValue(row,column);if(value==='')return'—';if(column.kind==='money_in')return Number(value)>0?`+${this.money(value)}`:'—';if(column.kind==='money_out')return Number(value)>0?`-${this.money(value)}`:'—';if(['balance','money'].indexOf(column.kind)!==-1)return this.money(value);return value;},cellClass(column){return{'is-number':this.isNumber(column.kind),'is-id':column.kind==='num','is-income':column.kind==='money_in','is-expense':column.kind==='money_out'};},isNumber(kind){return['money','money_in','money_out','balance','num'].indexOf(kind)!==-1;},money(value){return`¥${Number(value||0).toLocaleString('zh-CN',{minimumFractionDigits:2,maximumFractionDigits:2})}`;}, @@ -65,7 +81,7 @@ export default {